├── src ├── api │ ├── __init__.py │ └── auth_routes.py ├── utils │ ├── __init__.py │ └── time_formatters.py ├── config │ ├── __init__.py │ └── settings.py ├── models │ ├── __init__.py │ └── schemas.py ├── services │ ├── __init__.py │ ├── s3_service.py │ ├── database_service.py │ ├── auth_service.py │ └── summarization_service.py ├── core │ ├── __init__.py │ └── whisper_manager.py ├── __init__.py ├── realtime │ ├── __init__.py │ ├── models.py │ └── manager.py ├── main.py └── middleware │ └── auth_middleware.py ├── whisperx-fronted-docker-compose-extension ├── icons │ ├── icon128.png │ ├── icon16.png │ └── icon48.png ├── offscreen.html ├── config.js ├── INSTALL.md ├── permission.html ├── permission.js └── README.md ├── web_interface ├── cache_version.js ├── debug_config.js ├── server.py ├── modules │ ├── audio-processor.js │ ├── auth.js │ ├── fileHandler.js │ └── api.js ├── config.example.js ├── config.js ├── login.html └── index.html ├── requirements.txt ├── LICENSE ├── Dockerfile.frontend ├── .dockerignore ├── .env.example ├── QUICKSTART.md ├── .gitignore ├── Dockerfile ├── docker-compose.yml ├── DEPLOYMENT.md └── cursor-to-do ├── EXTENSION_DEVELOPMENT_PLAN.md ├── PROJECT_OVERVIEW.md ├── EXTENSION_TODO_PLAN.md └── REALTIME_DEVELOPMENT_PLAN.md /src/api/__init__.py: -------------------------------------------------------------------------------- 1 | # API модуль -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Утилиты -------------------------------------------------------------------------------- /src/config/__init__.py: -------------------------------------------------------------------------------- 1 | # Конфигурация -------------------------------------------------------------------------------- /src/models/__init__.py: -------------------------------------------------------------------------------- 1 | # Модели данных -------------------------------------------------------------------------------- /src/services/__init__.py: -------------------------------------------------------------------------------- 1 | # Сервисы -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- 1 | # Основные компоненты -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | redmadtrancribe x WhisperX API 3 | Структурированная версия API для транскрипции 4 | """ 5 | 6 | __version__ = "2.0.0" -------------------------------------------------------------------------------- /whisperx-fronted-docker-compose-extension/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vakovalskii/whisperx-fronted-docker-compose/HEAD/whisperx-fronted-docker-compose-extension/icons/icon128.png -------------------------------------------------------------------------------- /whisperx-fronted-docker-compose-extension/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vakovalskii/whisperx-fronted-docker-compose/HEAD/whisperx-fronted-docker-compose-extension/icons/icon16.png -------------------------------------------------------------------------------- /whisperx-fronted-docker-compose-extension/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vakovalskii/whisperx-fronted-docker-compose/HEAD/whisperx-fronted-docker-compose-extension/icons/icon48.png -------------------------------------------------------------------------------- /whisperx-fronted-docker-compose-extension/offscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Audio Recording Offscreen 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web_interface/cache_version.js: -------------------------------------------------------------------------------- 1 | // Cache buster для принудительного обновления конфигурации 2 | // Временная метка: 21 июня 2025 г. 22:47 (обновлен compute_type на auto) 3 | window.CACHE_VERSION = 1750646820; 4 | console.log('🔄 Cache buster активен. Версия:', window.CACHE_VERSION); -------------------------------------------------------------------------------- /src/realtime/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | whisperx-fronted-docker-compose Real-Time Transcription Module 3 | 4 | Модуль для real-time транскрипции аудио потоков. 5 | Интегрируется с существующей WhisperX инфраструктурой. 6 | """ 7 | 8 | __version__ = "0.1.0" 9 | __author__ = "whisperx-fronted-docker-compose Team" 10 | 11 | from .manager import RealtimeTranscriptionManager 12 | from .processor import StreamingAudioProcessor 13 | from .websocket_handler import RealtimeWebSocketHandler 14 | 15 | __all__ = [ 16 | "RealtimeTranscriptionManager", 17 | "StreamingAudioProcessor", 18 | "RealtimeWebSocketHandler" 19 | ] -------------------------------------------------------------------------------- /whisperx-fronted-docker-compose-extension/config.js: -------------------------------------------------------------------------------- 1 | // Chrome Extension Configuration 2 | const CONFIG = { 3 | // API endpoints - настройте под ваш сервер 4 | API_BASE: 'http://localhost:8880/api', 5 | FRONTEND_URL: 'http://localhost:8000', 6 | 7 | // Development mode 8 | DEVELOPMENT: true, 9 | 10 | // Permissions - обновите для вашего домена 11 | PERMISSIONS: [ 12 | 'http://localhost:8880/*', 13 | 'http://localhost:8000/*' 14 | // Для продакшена добавьте: 15 | // 'https://yourdomain.com/*', 16 | // 'https://api.yourdomain.com/*' 17 | ] 18 | }; 19 | 20 | // Export for use in other files 21 | if (typeof module !== 'undefined' && module.exports) { 22 | module.exports = CONFIG; 23 | } else { 24 | window.CONFIG = CONFIG; 25 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Основные зависимости FastAPI 2 | fastapi==0.104.1 3 | uvicorn[standard]==0.24.0 4 | pydantic[email]==2.5.0 5 | python-multipart==0.0.6 6 | 7 | # Google OAuth зависимости 8 | google-auth==2.23.4 9 | google-auth-oauthlib==1.1.0 10 | google-auth-httplib2==0.1.1 11 | authlib==1.2.1 12 | 13 | # JWT и криптография 14 | python-jose[cryptography]==3.3.0 15 | passlib[bcrypt]==1.7.4 16 | 17 | # HTTP клиент 18 | httpx==0.25.2 19 | requests==2.31.0 20 | 21 | # Middleware 22 | starlette==0.27.0 23 | itsdangerous==2.1.2 24 | 25 | # WhisperX и ML зависимости 26 | whisperx 27 | torch 28 | torchaudio 29 | transformers 30 | accelerate 31 | 32 | # Обработка файлов 33 | python-docx==1.1.0 34 | reportlab==4.0.7 35 | 36 | # AWS/S3 37 | boto3==1.34.0 38 | 39 | # Real-time зависимости 40 | numpy>=1.21.0 41 | websockets>=11.0.0 42 | scipy>=1.9.0 43 | 44 | # Утилиты 45 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /src/utils/time_formatters.py: -------------------------------------------------------------------------------- 1 | """ 2 | Утилиты для форматирования времени в различных форматах субтитров 3 | """ 4 | 5 | 6 | def format_time_srt(seconds: float) -> str: 7 | """Форматирование времени для SRT""" 8 | hours = int(seconds // 3600) 9 | minutes = int((seconds % 3600) // 60) 10 | secs = int(seconds % 60) 11 | millis = int((seconds % 1) * 1000) 12 | return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}" 13 | 14 | 15 | def format_time_vtt(seconds: float) -> str: 16 | """Форматирование времени для VTT""" 17 | hours = int(seconds // 3600) 18 | minutes = int((seconds % 3600) // 60) 19 | secs = seconds % 60 20 | return f"{hours:02d}:{minutes:02d}:{secs:06.3f}" 21 | 22 | 23 | def format_time_tsv(seconds: float) -> str: 24 | """Форматирование времени для TSV (в секундах с тремя знаками после запятой)""" 25 | return f"{seconds:.3f}" -------------------------------------------------------------------------------- /web_interface/debug_config.js: -------------------------------------------------------------------------------- 1 | // Debug скрипт для проверки конфигурации 2 | console.log('🔍 DEBUG: Конфигурация загружена'); 3 | console.log('📡 API BASE_URL:', CONFIG?.API?.BASE_URL); 4 | console.log('🔑 Endpoints:', CONFIG?.API?.ENDPOINTS); 5 | 6 | // Проверяем корректность URL 7 | if (CONFIG?.API?.BASE_URL) { 8 | if (CONFIG.API.BASE_URL.includes('production-domain.com')) { 9 | console.error('❌ ОШИБКА: Обнаружен дублированный домен в BASE_URL!'); 10 | console.error('Неправильный URL:', CONFIG.API.BASE_URL); 11 | alert('⚠️ Обнаружена ошибка в конфигурации API. Пожалуйста, очистите кеш браузера (Ctrl+Shift+R / Cmd+Shift+R)'); 12 | } else if (CONFIG.API.BASE_URL === 'http://localhost:8880') { 13 | console.log('✅ BASE_URL корректен:', CONFIG.API.BASE_URL); 14 | } else { 15 | console.warn('⚠️ BASE_URL не соответствует ожидаемому продакшн URL:', CONFIG.API.BASE_URL); 16 | } 17 | } 18 | 19 | // Выводим полную информацию для отладки 20 | window.DEBUG_CONFIG_INFO = { 21 | timestamp: new Date().toISOString(), 22 | baseUrl: CONFIG?.API?.BASE_URL, 23 | cacheVersion: window.CACHE_VERSION 24 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 whisperx-fronted-docker-compose Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile.frontend: -------------------------------------------------------------------------------- 1 | # whisperx-fronted-docker-compose Frontend - Статический веб-сервер 2 | FROM python:3.11-slim 3 | 4 | # Метаданные 5 | LABEL maintainer="whisperx-fronted-docker-compose Team" 6 | LABEL description="whisperx-fronted-docker-compose Frontend Web Interface" 7 | LABEL version="2.0.0" 8 | 9 | # Переменные окружения 10 | ENV PYTHONDONTWRITEBYTECODE=1 \ 11 | PYTHONUNBUFFERED=1 \ 12 | DEBIAN_FRONTEND=noninteractive 13 | 14 | # Устанавливаем минимальные системные зависимости 15 | RUN apt-get update && apt-get install -y \ 16 | curl \ 17 | && apt-get clean \ 18 | && rm -rf /var/lib/apt/lists/* 19 | 20 | # Создаем пользователя для безопасности 21 | RUN groupadd -r frontend && useradd -r -g frontend -d /app -s /bin/bash frontend 22 | 23 | # Создаем рабочую директорию 24 | WORKDIR /app 25 | 26 | # Копируем веб-интерфейс 27 | COPY --chown=frontend:frontend web_interface/ ./web_interface/ 28 | 29 | # Переключаемся на пользователя frontend 30 | USER frontend 31 | 32 | # Открываем порт 33 | EXPOSE 8000 34 | 35 | # Проверка здоровья контейнера 36 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 37 | CMD curl -f http://localhost:8000 || exit 1 38 | 39 | # Запускаем встроенный веб-сервер 40 | CMD ["python", "-m", "http.server", "8000", "--directory", "web_interface", "--bind", "0.0.0.0"] -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | .github 5 | 6 | # Python 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | *.so 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # Virtual environments 30 | whisperx_env_311/ 31 | venv/ 32 | env/ 33 | ENV/ 34 | env.bak/ 35 | venv.bak/ 36 | 37 | # IDE 38 | .vscode/ 39 | .idea/ 40 | *.swp 41 | *.swo 42 | *~ 43 | 44 | # OS 45 | .DS_Store 46 | .DS_Store? 47 | ._* 48 | .Spotlight-V100 49 | .Trashes 50 | ehthumbs.db 51 | Thumbs.db 52 | 53 | # Logs 54 | *.log 55 | logs/ 56 | 57 | # Temporary files 58 | *.tmp 59 | *.temp 60 | temp/ 61 | tmp/ 62 | 63 | # Docker 64 | Dockerfile* 65 | docker-compose*.yml 66 | .dockerignore 67 | 68 | # Documentation (кроме основного README) 69 | *.md 70 | !README.md 71 | 72 | # Figures and examples 73 | figures/ 74 | examples/ 75 | 76 | # Large files and caches 77 | *.model 78 | *.bin 79 | *.safetensors 80 | cache/ 81 | .cache/ 82 | 83 | # Data files (будут монтироваться как volumes) 84 | data/temp/* 85 | data/uploads/* 86 | !data/transcriptions_db.json 87 | 88 | # Node modules if any 89 | node_modules/ 90 | 91 | # Coverage reports 92 | htmlcov/ 93 | .tox/ 94 | .coverage 95 | .coverage.* 96 | .cache 97 | nosetests.xml 98 | coverage.xml 99 | *.cover 100 | .hypothesis/ 101 | .pytest_cache/ 102 | 103 | # Jupyter Notebook 104 | .ipynb_checkpoints 105 | 106 | # pyenv 107 | .python-version 108 | 109 | # Environment variables 110 | .env 111 | .env.local 112 | .env.development.local 113 | .env.test.local 114 | .env.production.local -------------------------------------------------------------------------------- /web_interface/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple HTTP Server for AI-Transcribe Web Interface 4 | Простой HTTP сервер для веб-интерфейса AI-Transcribe 5 | """ 6 | 7 | import http.server 8 | import socketserver 9 | import os 10 | import webbrowser 11 | from pathlib import Path 12 | from urllib.parse import urlparse 13 | 14 | # Конфигурация 15 | PORT = 8000 16 | DIRECTORY = Path(__file__).parent 17 | 18 | class CORSHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): 19 | """HTTP обработчик с поддержкой CORS и перенаправлением""" 20 | 21 | def end_headers(self): 22 | self.send_header('Access-Control-Allow-Origin', '*') 23 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 24 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 25 | super().end_headers() 26 | 27 | def do_OPTIONS(self): 28 | self.send_response(200) 29 | self.end_headers() 30 | 31 | def do_GET(self): 32 | # Перенаправляем корневой путь на страницу логина 33 | if self.path == '/': 34 | self.send_response(302) 35 | self.send_header('Location', '/login.html') 36 | self.end_headers() 37 | return 38 | 39 | # Обычная обработка для остальных путей 40 | super().do_GET() 41 | 42 | def main(): 43 | """Запуск сервера""" 44 | # Переходим в директорию с веб-файлами 45 | os.chdir(DIRECTORY) 46 | 47 | # Создаем сервер 48 | with socketserver.TCPServer(("0.0.0.0", PORT), CORSHTTPRequestHandler) as httpd: 49 | print(f"�� Запуск веб-сервера AI-Transcribe на порту {PORT}") 50 | print(f"📁 Директория: {DIRECTORY}") 51 | print(f"🌐 Локальный доступ: http://localhost:{PORT}") 52 | print(f"🌐 Сетевой доступ: http://0.0.0.0:{PORT}") 53 | print(f"🔐 Пароль для входа: AI-Transcribe") 54 | print("⏹️ Для остановки нажмите Ctrl+C") 55 | 56 | # Автоматически открыть браузер 57 | try: 58 | webbrowser.open(f'http://localhost:{PORT}') 59 | except: 60 | pass 61 | 62 | try: 63 | httpd.serve_forever() 64 | except KeyboardInterrupt: 65 | print("\n🛑 Сервер остановлен") 66 | 67 | if __name__ == "__main__": 68 | main() -------------------------------------------------------------------------------- /web_interface/modules/audio-processor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AudioWorklet процессор для real-time захвата аудио 3 | * 4 | * Этот модуль работает в отдельном потоке и обрабатывает аудио данные 5 | */ 6 | 7 | class AudioProcessor extends AudioWorkletProcessor { 8 | constructor() { 9 | super(); 10 | 11 | this.bufferSize = 0; 12 | this.buffer = []; 13 | this.sampleRate = 16000; // Целевая частота для WhisperX 14 | 15 | // Слушаем сообщения от главного потока 16 | this.port.onmessage = (event) => { 17 | if (event.data.command === 'updateConfig') { 18 | this.bufferSize = event.data.bufferSize; 19 | this.sampleRate = event.data.sampleRate; 20 | } 21 | }; 22 | } 23 | 24 | process(inputs, outputs, parameters) { 25 | const input = inputs[0]; 26 | 27 | if (input && input.length > 0) { 28 | const inputChannel = input[0]; // Берем первый канал (моно) 29 | 30 | // Добавляем данные в буфер 31 | for (let i = 0; i < inputChannel.length; i++) { 32 | this.buffer.push(inputChannel[i]); 33 | } 34 | 35 | // Если буфер достиг нужного размера, отправляем данные 36 | if (this.buffer.length >= this.bufferSize) { 37 | // Конвертируем float32 в int16 для отправки 38 | const int16Buffer = new Int16Array(this.buffer.length); 39 | for (let i = 0; i < this.buffer.length; i++) { 40 | // Ограничиваем значения и конвертируем в int16 41 | const sample = Math.max(-1, Math.min(1, this.buffer[i])); 42 | int16Buffer[i] = sample * 32767; 43 | } 44 | 45 | // Отправляем данные в главный поток 46 | this.port.postMessage({ 47 | type: 'audioData', 48 | data: int16Buffer, 49 | length: int16Buffer.length 50 | }); 51 | 52 | // Очищаем буфер 53 | this.buffer = []; 54 | } 55 | } 56 | 57 | return true; // Продолжаем обработку 58 | } 59 | } 60 | 61 | registerProcessor('audio-processor', AudioProcessor); -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # whisperx-fronted-docker-compose - Быстрый старт 2 | # Скопируйте в .env и заполните обязательные поля 3 | 4 | # === 🔐 GOOGLE OAUTH (ОБЯЗАТЕЛЬНО) === 5 | # Получите на https://console.cloud.google.com/apis/credentials 6 | GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com 7 | GOOGLE_CLIENT_SECRET=your_google_client_secret 8 | REDIRECT_URI=http://localhost:8880/api/auth/oauth/google/callback 9 | 10 | # === 🤖 AI СУММАРИЗАЦИЯ (ОБЯЗАТЕЛЬНО для vLLM) === 11 | # vLLM сервер с xgrammar поддержкой 12 | SUMMARIZATION_API_URL=http://localhost:11434/v1/chat/completions 13 | SUMMARIZATION_API_KEY=your-api-key-here 14 | SUMMARIZATION_MODEL=meta-llama/Llama-3.1-8B-Instruct 15 | SUMMARIZATION_MAX_TOKENS=4000 16 | SUMMARIZATION_TEMPERATURE=0.1 17 | 18 | # === ☁️ YANDEX CLOUD S3 (ОПЦИОНАЛЬНО) === 19 | # Для автоматического сохранения файлов в облако 20 | S3_ACCESS_KEY=your_s3_access_key 21 | S3_SECRET_KEY=your_s3_secret_key 22 | S3_BUCKET=your_s3_bucket 23 | S3_ENDPOINT=https://storage.yandexcloud.net 24 | S3_REGION=ru-central1 25 | 26 | # === 🎤 ДИАРИЗАЦИЯ СПИКЕРОВ (ОПЦИОНАЛЬНО) === 27 | # Токен HuggingFace для разделения спикеров 28 | # Получите на https://huggingface.co/settings/tokens 29 | HF_TOKEN=your_huggingface_token 30 | 31 | # === ⚙️ ОСНОВНЫЕ НАСТРОЙКИ === 32 | ENVIRONMENT=production 33 | DEBUG=false 34 | HOST=0.0.0.0 35 | BACKEND_PORT=8880 36 | FRONTEND_PORT=8000 37 | FRONTEND_URL=http://localhost:8000 38 | BACKEND_URL=http://localhost:8880 39 | 40 | # === 🔑 БЕЗОПАСНОСТЬ === 41 | # Генерируйте: openssl rand -hex 32 42 | JWT_SECRET_KEY=your_jwt_secret_key_here 43 | 44 | # === 🎯 WHISPERX НАСТРОЙКИ === 45 | WHISPERX_MODEL=large-v3 46 | WHISPERX_LANGUAGE=ru 47 | WHISPERX_COMPUTE_TYPE=float16 48 | WHISPERX_BATCH_SIZE=16 49 | 50 | # ===================================== 51 | # 📝 ИНСТРУКЦИИ ПО НАСТРОЙКЕ: 52 | # ===================================== 53 | # 54 | # 1. Google OAuth: 55 | # - Создайте проект на https://console.cloud.google.com 56 | # - Включите Google+ API 57 | # - Создайте OAuth 2.0 credentials 58 | # - Добавьте redirect URI: http://localhost:8880/api/auth/oauth/google/callback 59 | # 60 | # 2. vLLM сервер (для AI суммаризации): 61 | # docker run --gpus all -p 11434:8000 \ 62 | # vllm/vllm-openai:latest \ 63 | # --model meta-llama/Llama-3.1-8B-Instruct \ 64 | # --guided-decoding-backend xgrammar 65 | # 66 | # 3. Запуск приложения: 67 | # docker-compose build 68 | # docker-compose up -d 69 | # 70 | # 4. Откройте: http://localhost:8000 71 | -------------------------------------------------------------------------------- /whisperx-fronted-docker-compose-extension/INSTALL.md: -------------------------------------------------------------------------------- 1 | # 🚀 Быстрая установка и тестирование 2 | 3 | ## 📦 Установка расширения 4 | 5 | 1. **Откройте Chrome** и перейдите по адресу: `chrome://extensions/` 6 | 7 | 2. **Включите режим разработчика:** 8 | - Переключите тумблер "Режим разработчика" (Developer mode) в правом верхнем углу 9 | 10 | 3. **Загрузите расширение:** 11 | - Нажмите кнопку "Загрузить распакованное расширение" (Load unpacked) 12 | - Выберите папку `whisperx-fronted-docker-compose-extension` (эту папку) 13 | - Нажмите "Выбрать" (Select Folder) 14 | 15 | 4. **Готово!** Вы увидите расширение "Audio Recorder Extension" в списке 16 | 17 | ## 🧪 Быстрое тестирование 18 | 19 | ### Шаг 1: Получение разрешения на микрофон 20 | 1. Нажмите на иконку расширения 🎙️ в панели инструментов Chrome (справа от адресной строки) 21 | 2. В popup нажмите **"Request Microphone Permission"** 22 | 3. Откроется новая вкладка - нажмите **"Allow Microphone Access"** 23 | 4. В диалоге браузера нажмите **"Разрешить"** (Allow) 24 | 5. Вкладка закроется, вернитесь к popup расширения 25 | 26 | ### Шаг 2: Тестовая запись 27 | 1. **Откройте YouTube** в новой вкладке 28 | 2. **Включите любое видео** с звуком 29 | 3. **Нажмите на иконку расширения** 🎙️ 30 | 4. **Нажмите "🎙️ Start Recording"** 31 | 5. **Говорите в микрофон** - расширение записывает и ваш голос, и звук видео 32 | 6. **Нажмите "⏹️ Stop & Save"** через несколько секунд 33 | 7. **Выберите место сохранения** файла (рекомендуется Рабочий стол) 34 | 35 | ### Шаг 3: Проверка записи 36 | 1. Найдите сохраненный файл (например: `audio-recording-2025-06-24T19-30-45.webm`) 37 | 2. Откройте его в любом медиаплеере 38 | 3. Вы должны услышать и свой голос, и звук из YouTube видео 39 | 40 | ## ❗ Возможные проблемы 41 | 42 | ### Проблема: Кнопка "Start Recording" неактивна 43 | **Решение:** Убедитесь, что разрешение на микрофон получено (зеленая галочка в popup) 44 | 45 | ### Проблема: Ошибка при начале записи 46 | **Решение:** 47 | 1. Убедитесь, что вкладка с аудио контентом активна 48 | 2. Проверьте, что звук действительно воспроизводится 49 | 3. Перезагрузите расширение в `chrome://extensions/` 50 | 51 | ### Проблема: Файл не воспроизводится 52 | **Решение:** 53 | 1. Используйте VLC Player или другой медиаплеер с поддержкой WebM 54 | 2. Или конвертируйте файл в MP3 с помощью онлайн-конвертера 55 | 56 | ## 🎯 Готово к использованию! 57 | 58 | Теперь вы можете записывать: 59 | - Видеозвонки (Zoom, Meet, Teams) 60 | - Подкасты и музыку 61 | - Обучающие видео 62 | - Любой контент с одновременным комментированием 63 | 64 | **Удачной записи! 🎵** -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # ⚡ Быстрый старт - 5 минут до запуска 2 | 3 | ## 📋 Что нужно: 4 | - **Ubuntu 20.04+** 5 | - **NVIDIA GPU 8GB+** 6 | - **Docker** + **NVIDIA Container Toolkit** 7 | - **NVIDIA драйверы 470+** 8 | 9 | ## 🚀 Запуск: 10 | 11 | ### 1. Клонируем и настраиваем 12 | ```bash 13 | git clone https://github.com/your-repo/whisperx-fronted-docker-compose 14 | cd whisperx-fronted-docker-compose 15 | cp .env.example .env 16 | ``` 17 | 18 | ### 2. Настраиваем Google OAuth 19 | ```bash 20 | # Идем на https://console.cloud.google.com/apis/credentials 21 | # Создаем OAuth 2.0 Client ID 22 | # Добавляем redirect URI: http://localhost:8880/api/auth/oauth/google/callback 23 | # Копируем Client ID и Secret в .env файл 24 | ``` 25 | 26 | ### 3. Запускаем vLLM (для AI суммаризации) 27 | ```bash 28 | # В отдельном терминале 29 | docker run --gpus all -p 11434:8000 \ 30 | vllm/vllm-openai:latest \ 31 | --model meta-llama/Llama-3.1-8B-Instruct \ 32 | --guided-decoding-backend xgrammar 33 | ``` 34 | 35 | ### 4. Запускаем приложение 36 | ```bash 37 | docker-compose build 38 | docker-compose up -d 39 | ``` 40 | 41 | ### 5. Готово! 42 | Открываем http://localhost:8000 43 | 44 | ## 🔧 Минимальная настройка .env: 45 | 46 | ```bash 47 | # Обязательно заполнить: 48 | GOOGLE_CLIENT_ID=ваш-google-client-id 49 | GOOGLE_CLIENT_SECRET=ваш-google-client-secret 50 | 51 | # Остальное можно оставить как есть 52 | SUMMARIZATION_API_URL=http://localhost:11434/v1/chat/completions 53 | SUMMARIZATION_MODEL=meta-llama/Llama-3.1-8B-Instruct 54 | ``` 55 | 56 | ## ❓ Проблемы? 57 | 58 | **GPU не найдена:** 59 | ```bash 60 | # Установите NVIDIA Container Toolkit 61 | curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg 62 | curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ 63 | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ 64 | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list 65 | sudo apt update && sudo apt install -y nvidia-container-toolkit 66 | sudo systemctl restart docker 67 | ``` 68 | 69 | **vLLM не запускается:** 70 | ```bash 71 | # Проверьте что модель поддерживает 32K+ токенов 72 | # Llama-3.1-8B-Instruct поддерживает 128K токенов 73 | ``` 74 | 75 | **OAuth ошибка:** 76 | - Проверьте что redirect URI точно совпадает 77 | - Убедитесь что Google+ API включен в проекте 78 | 79 | ## 🎯 Что получите: 80 | - ✅ Транскрипция аудио/видео 81 | - ✅ Разделение спикеров 82 | - ✅ AI суммаризация 83 | - ✅ 6 форматов экспорта 84 | - ✅ Real-time транскрипция 85 | - ✅ Chrome расширение -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | .env.local 4 | .env.production 5 | .env.development 6 | 7 | # Python 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | *.so 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | 116 | # Data files 117 | data/ 118 | uploads/ 119 | temp/ 120 | *.json 121 | !requirements.txt 122 | !package*.json 123 | 124 | # Logs 125 | *.log 126 | logs/ 127 | 128 | # OS 129 | .DS_Store 130 | .DS_Store? 131 | ._* 132 | .Spotlight-V100 133 | .Trashes 134 | ehthumbs.db 135 | Thumbs.db 136 | 137 | # IDE 138 | .vscode/ 139 | .idea/ 140 | *.swp 141 | *.swo 142 | *~ 143 | 144 | # Node.js 145 | node_modules/ 146 | npm-debug.log* 147 | yarn-debug.log* 148 | yarn-error.log* 149 | 150 | # Chrome Extension 151 | whisperx-fronted-docker-compose-extension.zip 152 | *.crx 153 | *.pem 154 | *.key 155 | *.crt 156 | *.p12 157 | 158 | # Temporary files 159 | *.tmp 160 | *.temp 161 | temp_* 162 | 163 | # Media files 164 | *.mp3 165 | *.mp4 166 | *.wav 167 | *.webm 168 | *.m4a 169 | *.avi 170 | *.mkv 171 | 172 | # Documentation temp files 173 | *.docx 174 | *.pdf 175 | *.srt 176 | *.vtt 177 | *.tsv -------------------------------------------------------------------------------- /whisperx-fronted-docker-compose-extension/permission.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Microphone Permission Request 6 | 75 | 76 | 77 |
78 |
🎙️
79 |

Microphone Permission Required

80 |
81 | This extension needs access to your microphone to record audio. 82 |

83 | Please click "Allow Microphone Access" to grant permission. 84 |
85 | Your browser will show a permission dialog. 86 |
87 | 88 | 89 | 90 | 91 | 92 |
93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /whisperx-fronted-docker-compose-extension/permission.js: -------------------------------------------------------------------------------- 1 | class MicrophonePermissionHandler { 2 | constructor() { 3 | this.allowBtn = document.getElementById('allowBtn'); 4 | this.denyBtn = document.getElementById('denyBtn'); 5 | this.status = document.getElementById('status'); 6 | 7 | this.initEventListeners(); 8 | } 9 | 10 | initEventListeners() { 11 | this.allowBtn.addEventListener('click', () => this.requestPermission()); 12 | this.denyBtn.addEventListener('click', () => this.denyPermission()); 13 | } 14 | 15 | async requestPermission() { 16 | this.showStatus('Requesting microphone permission...', ''); 17 | this.allowBtn.disabled = true; 18 | this.denyBtn.disabled = true; 19 | 20 | try { 21 | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 22 | 23 | // Получили разрешение, останавливаем поток 24 | stream.getTracks().forEach(track => track.stop()); 25 | 26 | this.showStatus('✅ Microphone permission granted!', 'success'); 27 | 28 | // Отправляем результат в background script 29 | chrome.runtime.sendMessage({ 30 | type: 'permissionResult', 31 | granted: true 32 | }); 33 | 34 | // Закрываем страницу через 2 секунды 35 | setTimeout(() => { 36 | window.close(); 37 | }, 2000); 38 | 39 | } catch (error) { 40 | console.error('Microphone permission denied:', error); 41 | this.showStatus('❌ Microphone permission denied', 'error'); 42 | 43 | // Отправляем результат в background script 44 | chrome.runtime.sendMessage({ 45 | type: 'permissionResult', 46 | granted: false 47 | }); 48 | 49 | this.allowBtn.disabled = false; 50 | this.denyBtn.disabled = false; 51 | } 52 | } 53 | 54 | denyPermission() { 55 | this.showStatus('❌ Permission denied by user', 'error'); 56 | 57 | // Отправляем результат в background script 58 | chrome.runtime.sendMessage({ 59 | type: 'permissionResult', 60 | granted: false 61 | }); 62 | 63 | // Закрываем страницу через 2 секунды 64 | setTimeout(() => { 65 | window.close(); 66 | }, 2000); 67 | } 68 | 69 | showStatus(message, className) { 70 | this.status.textContent = message; 71 | this.status.className = `status ${className}`; 72 | this.status.style.display = 'block'; 73 | } 74 | } 75 | 76 | // Инициализируем обработчик когда DOM загружен 77 | document.addEventListener('DOMContentLoaded', () => { 78 | new MicrophonePermissionHandler(); 79 | }); -------------------------------------------------------------------------------- /src/config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Конфигурация приложения 3 | """ 4 | import os 5 | from pathlib import Path 6 | 7 | # Базовые пути 8 | BASE_DIR = Path(__file__).parent.parent.parent 9 | DATA_DIR = BASE_DIR / "data" 10 | UPLOADS_DIR = DATA_DIR / "uploads" 11 | TRANSCRIPTS_DIR = DATA_DIR / "transcripts" 12 | TEMP_DIR = DATA_DIR / "temp" 13 | DATABASE_FILE = DATA_DIR / "transcriptions_db.json" 14 | 15 | # Создаем директории 16 | for dir_path in [DATA_DIR, UPLOADS_DIR, TRANSCRIPTS_DIR, TEMP_DIR]: 17 | dir_path.mkdir(parents=True, exist_ok=True) 18 | 19 | # Конфигурация S3 (Yandex Cloud) 20 | S3_CONFIG = { 21 | 'aws_access_key_id': os.getenv('S3_ACCESS_KEY', ''), 22 | 'aws_secret_access_key': os.getenv('S3_SECRET_KEY', ''), 23 | 'bucket_name': os.getenv('S3_BUCKET', 'your-bucket-name'), 24 | 'endpoint_url': os.getenv('S3_ENDPOINT', 'https://storage.yandexcloud.net'), 25 | 'region_name': os.getenv('S3_REGION', 'ru-central1') 26 | } 27 | 28 | # OAuth конфигурация 29 | OAUTH_CONFIG = { 30 | 'google_client_id': os.getenv('GOOGLE_CLIENT_ID', ''), 31 | 'google_client_secret': os.getenv('GOOGLE_CLIENT_SECRET', ''), 32 | 'redirect_uri': os.getenv('REDIRECT_URI', 'http://localhost:8880/api/auth/oauth/google/callback'), 33 | 'scopes': ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile'] 34 | } 35 | 36 | # JWT конфигурация 37 | JWT_CONFIG = { 38 | 'secret_key': os.getenv('JWT_SECRET_KEY', ''), 39 | 'algorithm': 'HS256', 40 | 'access_token_expire_minutes': 60 * 24 * 7, # 7 дней 41 | 'refresh_token_expire_minutes': 60 * 24 * 30 # 30 дней 42 | } 43 | 44 | # Поддерживаемые форматы 45 | SUPPORTED_FORMATS = { 46 | # Аудио 47 | 'mp3', 'm4a', 'wav', 'flac', 'ogg', 'wma', 'aac', 'opus', 48 | # Видео 49 | 'mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm', '3gp', 'mts' 50 | } 51 | 52 | # Настройки сервера 53 | SERVER_CONFIG = { 54 | 'host': '0.0.0.0', 55 | 'port': 8880, 56 | 'reload': False, 57 | 'log_level': 'info' 58 | } 59 | 60 | # CORS настройки 61 | CORS_ORIGINS = [ 62 | "http://localhost:8000", 63 | "http://localhost:8880", 64 | "http://localhost:8000", 65 | "http://127.0.0.1:8000", 66 | "http://0.0.0.0:8000", 67 | "http://localhost:8880", 68 | "http://127.0.0.1:8880", 69 | "*" 70 | ] 71 | 72 | # Настройки обработки 73 | PROCESSING_CONFIG = { 74 | 'max_workers': 2, 75 | 'default_model': 'large-v3', 76 | 'default_language': 'ru', 77 | 'default_compute_type': 'float16', 78 | 'default_batch_size': 16 79 | } 80 | 81 | # Настройки суммаризации 82 | SUMMARIZATION_CONFIG = { 83 | 'api_url': os.getenv('SUMMARIZATION_API_URL', 'http://localhost:11434/v1/chat/completions'), 84 | 'api_key': os.getenv('SUMMARIZATION_API_KEY', 'your-api-key-here'), 85 | 'model': os.getenv('SUMMARIZATION_MODEL', 'llama3.1:8b'), 86 | 'max_tokens': int(os.getenv('SUMMARIZATION_MAX_TOKENS', '4000')), 87 | 'temperature': float(os.getenv('SUMMARIZATION_TEMPERATURE', '0.1')) 88 | } -------------------------------------------------------------------------------- /web_interface/config.example.js: -------------------------------------------------------------------------------- 1 | // Пример конфигурации AI-Transcribe Web Interface 2 | // Скопируйте этот файл в config.js и настройте под свои нужды 3 | 4 | const CONFIG = { 5 | // API настройки 6 | API: { 7 | // Продакшн адрес API сервера 8 | BASE_URL: 'http://localhost:8880', 9 | 10 | // Ваш токен HuggingFace для диаризации спикеров 11 | HF_TOKEN: 'your-huggingface-token-here', 12 | 13 | ENDPOINTS: { 14 | UPLOAD: '/upload', 15 | STATUS: '/status', 16 | TRANSCRIPTIONS: '/transcriptions', 17 | DOWNLOAD_AUDIO: '/download/audio', 18 | DOWNLOAD_TRANSCRIPT: '/download/transcript', 19 | DOWNLOAD_SUBTITLE: '/download/subtitle', 20 | DELETE_TRANSCRIPTION: '/transcription', 21 | S3_LINKS: '/s3-links', // Эндпоинт для получения S3 ссылок 22 | SUMMARIZE: '/summarize', // Эндпоинт для суммаризации 23 | SUMMARIZATION_CONFIG: '/config/summarization' // Эндпоинт для конфигурации суммаризации 24 | } 25 | }, 26 | 27 | // Настройки авторизации 28 | AUTH: { 29 | // Пароль для входа в систему 30 | PASSWORD: 'your-secure-password', 31 | 32 | // Длительность сессии в миллисекундах (по умолчанию 24 часа) 33 | SESSION_DURATION: 24 * 60 * 60 * 1000, 34 | 35 | STORAGE_KEYS: { 36 | IS_LOGGED_IN: 'isLoggedIn', 37 | LOGIN_TIME: 'loginTime' 38 | } 39 | }, 40 | 41 | // Настройки интерфейса 42 | UI: { 43 | // Интервал обновления прогресса транскрипции (мс) 44 | PROGRESS_UPDATE_INTERVAL: 2000, 45 | 46 | // Длительность показа уведомлений (мс) 47 | NOTIFICATION_DURATION: 3000, 48 | INFO_NOTIFICATION_DURATION: 5000, 49 | 50 | // Таймаут загрузки медиа файлов (мс) 51 | MEDIA_LOADING_TIMEOUT: 30000, 52 | 53 | // Настройки по умолчанию 54 | AUTO_SCROLL: true, 55 | SHOW_TIMESTAMPS: true 56 | }, 57 | 58 | // Настройки транскрипции по умолчанию 59 | TRANSCRIPTION: { 60 | DEFAULT_MODEL: 'large-v3', 61 | DEFAULT_LANGUAGE: 'ru', 62 | DEFAULT_DIARIZE: false, 63 | DEFAULT_COMPUTE_TYPE: 'auto', 64 | DEFAULT_BATCH_SIZE: 16 65 | }, 66 | 67 | // Настройки суммаризации перенесены в переменные окружения сервера 68 | // Конфигурация теперь получается через API: /api/config/summarization 69 | // Настройте переменные окружения в .env файле: 70 | // SUMMARIZATION_API_URL, SUMMARIZATION_API_KEY, SUMMARIZATION_MODEL, и т.д. 71 | 72 | // Настройки разработки 73 | DEBUG: { 74 | // Включить отладочные сообщения в консоль 75 | ENABLED: false, 76 | 77 | // Логировать все API вызовы 78 | LOG_API_CALLS: false, 79 | 80 | // Симулировать медленную сеть для тестирования 81 | SIMULATE_SLOW_NETWORK: false 82 | } 83 | }; 84 | 85 | // Экспортируем конфигурацию 86 | if (typeof module !== 'undefined' && module.exports) { 87 | module.exports = CONFIG; 88 | } -------------------------------------------------------------------------------- /src/models/schemas.py: -------------------------------------------------------------------------------- 1 | """ 2 | Модели данных для API 3 | """ 4 | from typing import List, Optional, Dict, Any 5 | from pydantic import BaseModel, EmailStr # Включено обратно 6 | from datetime import datetime 7 | 8 | 9 | # Модели аутентификации 10 | class GoogleUser(BaseModel): 11 | """Модель пользователя Google""" 12 | email: EmailStr # Включено обратно 13 | name: str 14 | picture: Optional[str] = None 15 | google_id: str 16 | locale: Optional[str] = None 17 | 18 | 19 | class User(BaseModel): 20 | """Модель пользователя в системе""" 21 | id: str 22 | email: EmailStr # Включено обратно 23 | name: str 24 | picture: Optional[str] = None 25 | google_id: str 26 | created_at: datetime 27 | last_login: datetime 28 | is_active: bool = True 29 | 30 | 31 | class UserSession(BaseModel): 32 | """Модель пользовательской сессии""" 33 | user_id: str 34 | session_token: str 35 | expires_at: datetime 36 | created_at: datetime 37 | user_agent: Optional[str] = None 38 | ip_address: Optional[str] = None 39 | 40 | 41 | class AuthResponse(BaseModel): 42 | """Ответ при аутентификации""" 43 | access_token: str 44 | token_type: str = "bearer" 45 | expires_in: int 46 | user_info: User 47 | 48 | 49 | class TokenData(BaseModel): 50 | """Данные из JWT токена""" 51 | user_id: Optional[str] = None 52 | email: Optional[str] = None 53 | 54 | 55 | # Существующие модели транскрипции 56 | class TranscriptionConfig(BaseModel): 57 | """Конфигурация для транскрипции""" 58 | model: str = "large-v3" 59 | language: str = "ru" 60 | diarize: bool = False 61 | hf_token: Optional[str] = None 62 | compute_type: str = "auto" 63 | batch_size: int = 16 64 | 65 | 66 | class TranscriptionStatus(BaseModel): 67 | """Статус транскрипции""" 68 | id: str 69 | status: str # pending, processing, completed, failed 70 | filename: str 71 | created_at: str 72 | completed_at: Optional[str] = None 73 | error: Optional[str] = None 74 | progress: Optional[str] = None 75 | progress_percent: Optional[int] = None 76 | user_id: Optional[str] = None # Добавляем связь с пользователем 77 | 78 | 79 | class TranscriptionResult(BaseModel): 80 | """Результат транскрипции""" 81 | id: str 82 | filename: str 83 | status: str 84 | created_at: str 85 | completed_at: Optional[str] = None 86 | transcript_file: Optional[str] = None 87 | audio_file: Optional[str] = None 88 | subtitle_files: Optional[Dict[str, str]] = None 89 | s3_links: Optional[Dict[str, str]] = None 90 | segments: Optional[List[Dict[str, Any]]] = None 91 | error: Optional[str] = None 92 | progress: Optional[str] = None 93 | progress_percent: Optional[int] = None 94 | user_id: Optional[str] = None # Добавляем связь с пользователем 95 | 96 | 97 | class TranscriptionListItem(BaseModel): 98 | """Элемент списка транскрипций""" 99 | id: str 100 | filename: str 101 | status: str 102 | created_at: str 103 | completed_at: Optional[str] = None 104 | transcript_file: Optional[str] = None 105 | audio_file: Optional[str] = None 106 | subtitle_files: Optional[Dict[str, str]] = None 107 | s3_links: Optional[Dict[str, str]] = None 108 | error: Optional[str] = None 109 | progress: Optional[str] = None 110 | progress_percent: Optional[int] = None 111 | user_id: Optional[str] = None # Добавляем связь с пользователем -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Главный файл приложения FastAPI 3 | whisperx-fronted-docker-compose - AI Транскрипция с Google OAuth 4 | 5 | Основано на WhisperX by Max Bain (https://github.com/m-bain/whisperX) 6 | Лицензия WhisperX: BSD-2-Clause 7 | """ 8 | from fastapi import FastAPI 9 | from fastapi.middleware.cors import CORSMiddleware 10 | from starlette.middleware.sessions import SessionMiddleware # Включено обратно 11 | 12 | from .api.routes import router 13 | from .api.auth_routes import router as auth_router # Включено обратно 14 | from .api.realtime_routes import router as realtime_router, initialize_realtime_system, shutdown_realtime_system # Real-time маршруты 15 | from .config.settings import CORS_ORIGINS, JWT_CONFIG 16 | 17 | 18 | def create_app() -> FastAPI: 19 | """Создание и настройка FastAPI приложения""" 20 | 21 | app = FastAPI( 22 | title="whisperx-fronted-docker-compose - AI Транскрипция с Google OAuth", 23 | description="API для транскрипции аудио и видео файлов с экспортом в 6 форматов, Google OAuth аутентификацией и автоматической загрузкой на Yandex Cloud S3. Каждый пользователь видит только свои транскрипции.", 24 | version="2.1.0" 25 | ) 26 | 27 | # Добавляем Session middleware для OAuth 28 | app.add_middleware( 29 | SessionMiddleware, 30 | secret_key=JWT_CONFIG['secret_key'] 31 | ) 32 | 33 | # Добавляем CORS middleware 34 | app.add_middleware( 35 | CORSMiddleware, 36 | allow_origins=CORS_ORIGINS, 37 | allow_credentials=True, 38 | allow_methods=["GET", "POST", "DELETE", "OPTIONS"], 39 | allow_headers=["*"] 40 | ) 41 | 42 | # Подключаем роуты 43 | app.include_router(auth_router, prefix="/api", tags=["Аутентификация"]) # Включено обратно 44 | app.include_router(router, prefix="/api", tags=["Транскрипция"]) 45 | app.include_router(realtime_router, prefix="/api", tags=["Real-Time Транскрипция"]) # Real-time маршруты 46 | 47 | @app.on_event("startup") 48 | async def startup_event(): 49 | """Инициализация при запуске""" 50 | print("🚀 Запуск whisperx-fronted-docker-compose - AI Транскрипция с Google OAuth v2.1...") 51 | print("🔐 Google OAuth аутентификация включена") 52 | print("👥 Персональные транскрипции для каждого пользователя") 53 | print("🌐 CORS настроен для всех доменов") 54 | print("📋 Доступные форматы экспорта: JSON, SRT, VTT, TSV, DOCX, PDF") 55 | print("☁️ Автоматическая загрузка файлов на Yandex Cloud S3") 56 | print("🗑️ Автоматическая очистка локальных файлов после загрузки на S3") 57 | print("💾 JSON база данных для метаданных транскрипций и пользователей") 58 | print("🎙️ Real-time транскрипция включена (WebSocket: /api/realtime/ws)") 59 | 60 | # Инициализация real-time системы 61 | try: 62 | await initialize_realtime_system() 63 | print("✅ Real-time система инициализирована") 64 | except Exception as e: 65 | print(f"⚠️ Ошибка инициализации real-time системы: {e}") 66 | 67 | print("✅ Сервер готов к работе! Модели будут загружены при первом запросе.") 68 | 69 | @app.on_event("shutdown") 70 | async def shutdown_event(): 71 | """Очистка ресурсов при остановке""" 72 | print("🔄 Остановка сервера...") 73 | try: 74 | await shutdown_realtime_system() 75 | print("✅ Real-time система остановлена") 76 | except Exception as e: 77 | print(f"⚠️ Ошибка остановки real-time системы: {e}") 78 | print("👋 Сервер остановлен") 79 | 80 | return app 81 | 82 | 83 | # Создаем экземпляр приложения 84 | app = create_app() -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # whisperx-fronted-docker-compose - AI Транскрипция с GPU поддержкой 2 | # Основано на WhisperX by Max Bain (https://github.com/m-bain/whisperX) 3 | # Базовый образ PyTorch с CUDA и cuDNN для GPU ускорения 4 | 5 | FROM pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel as base 6 | 7 | # Метаданные 8 | LABEL maintainer="whisperx-fronted-docker-compose Team" 9 | LABEL description="AI-powered transcription service with WhisperX and GPU support" 10 | LABEL version="2.0.0" 11 | 12 | # Переменные окружения 13 | ENV PYTHONDONTWRITEBYTECODE=1 \ 14 | PYTHONUNBUFFERED=1 \ 15 | DEBIAN_FRONTEND=noninteractive \ 16 | PIP_NO_CACHE_DIR=1 \ 17 | PIP_DISABLE_PIP_VERSION_CHECK=1 \ 18 | NVIDIA_VISIBLE_DEVICES=all \ 19 | NVIDIA_DRIVER_CAPABILITIES=compute,utility 20 | 21 | # Устанавливаем системные зависимости 22 | RUN apt-get update && apt-get install -y \ 23 | # Основные системные пакеты 24 | build-essential \ 25 | curl \ 26 | git \ 27 | wget \ 28 | # Аудио/видео обработка 29 | ffmpeg \ 30 | libsndfile1 \ 31 | # Очистка кеша 32 | && apt-get clean \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | # Исправляем проблему совместимости cuDNN 36 | # Создаем символические ссылки для cuDNN 8.x совместимости 37 | RUN cd /opt/conda/lib/python*/site-packages/nvidia/cudnn/lib && \ 38 | ln -sf libcudnn.so.9 libcudnn.so.8 && \ 39 | ln -sf libcudnn_ops.so.9 libcudnn_ops_infer.so.8 && \ 40 | ln -sf libcudnn_cnn.so.9 libcudnn_cnn_infer.so.8 && \ 41 | ln -sf libcudnn_adv.so.9 libcudnn_adv_infer.so.8 && \ 42 | echo "cuDNN compatibility links created" 43 | 44 | # Обновляем LD_LIBRARY_PATH для поиска cuDNN библиотек 45 | ENV LD_LIBRARY_PATH="/opt/conda/lib/python3.11/site-packages/nvidia/cudnn/lib:${LD_LIBRARY_PATH}" 46 | 47 | # Создаем пользователя для безопасности 48 | RUN groupadd -r whisperx && useradd -r -g whisperx -d /app -s /bin/bash whisperx 49 | 50 | # Создаем рабочую директорию 51 | WORKDIR /app 52 | 53 | # Копируем весь проект для установки 54 | COPY . . 55 | 56 | # Устанавливаем uv для быстрой установки зависимостей 57 | RUN python -m pip install --upgrade pip && pip install uv 58 | 59 | # Устанавливаем Python зависимости из pyproject.toml 60 | # Принудительно переустанавливаем PyTorch с правильной версией 61 | RUN uv pip install --system --no-cache \ 62 | "torch>=2.6.0" \ 63 | "torchaudio>=2.6.0" \ 64 | "torchvision>=0.21.0" 65 | 66 | # Устанавливаем остальные зависимости из requirements.txt 67 | RUN uv pip install --system --no-cache -r requirements.txt 68 | 69 | # Создаем необходимые директории и устанавливаем права 70 | RUN mkdir -p data/temp data/uploads data/transcripts && \ 71 | mkdir -p /root/.cache/whisperx /root/.cache/huggingface /root/.cache/torch && \ 72 | chown -R whisperx:whisperx /app 73 | 74 | # Переключаемся на пользователя whisperx 75 | USER whisperx 76 | 77 | # Создаем init script для установки прав на volume при старте 78 | RUN echo '#!/bin/bash\n\ 79 | # Устанавливаем права на директории, которые могут быть volume\n\ 80 | if [ -d "/app/data" ]; then\n\ 81 | # Проверяем, есть ли права записи\n\ 82 | if [ ! -w "/app/data" ]; then\n\ 83 | echo "⚠️ Директория /app/data не доступна для записи. Попытка исправить..."\n\ 84 | # Создаем файл с правами, если его нет\n\ 85 | touch /app/data/.permissions_test 2>/dev/null || echo "❌ Нет прав записи в /app/data"\n\ 86 | fi\n\ 87 | # Создаем необходимые поддиректории\n\ 88 | mkdir -p /app/data/temp /app/data/uploads /app/data/transcripts 2>/dev/null || true\n\ 89 | fi\n\ 90 | exec "$@"' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh 91 | 92 | # Открываем порты 93 | EXPOSE 8880 8000 94 | 95 | # Проверка здоровья контейнера 96 | HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ 97 | CMD curl -f http://localhost:8880/health || exit 1 98 | 99 | # Точка входа для установки прав 100 | ENTRYPOINT ["/app/entrypoint.sh"] 101 | 102 | # Команда по умолчанию 103 | CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8880"] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | # GPU overlay для docker-compose.yml 4 | # Использование: docker-compose.yml 5 | 6 | services: 7 | # WhisperX Backend API с GPU поддержкой 8 | whisperx-backend: 9 | build: 10 | context: . 11 | dockerfile: Dockerfile 12 | container_name: whisperx-backend 13 | restart: unless-stopped 14 | ports: 15 | - "8880:8880" # Backend API 16 | volumes: 17 | # Монтирование исходного кода для разработки на лету 18 | - ./src:/app/src:rw 19 | - ./server.py:/app/server.py:rw 20 | 21 | # Монтирование данных (локальная директория для доступа) 22 | - ./data:/app/data 23 | 24 | # Монтирование моделей WhisperX (кеширование) 25 | - whisperx_models:/root/.cache/whisperx 26 | 27 | # Монтирование Hugging Face кеша 28 | - whisperx_hf_cache:/root/.cache/huggingface 29 | 30 | # Монтирование torch кеша 31 | - whisperx_torch_cache:/root/.cache/torch 32 | environment: 33 | # Основные настройки 34 | - PYTHONPATH=/app 35 | - ENVIRONMENT=production 36 | 37 | # Настройки сервера 38 | - HOST=0.0.0.0 39 | - PORT=8880 40 | 41 | # Настройки S3 (Yandex Cloud) - используйте .env файл 42 | - S3_ACCESS_KEY=${S3_ACCESS_KEY} 43 | - S3_SECRET_KEY=${S3_SECRET_KEY} 44 | - S3_BUCKET=${S3_BUCKET} 45 | - S3_ENDPOINT=https://storage.yandexcloud.net 46 | - S3_REGION=ru-central1 47 | 48 | # Google OAuth настройки 49 | - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} 50 | - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} 51 | - REDIRECT_URI=${REDIRECT_URI} 52 | 53 | # JWT настройки 54 | - JWT_SECRET_KEY=${JWT_SECRET_KEY} 55 | 56 | # Настройки WhisperX (автоматическое определение compute_type) 57 | - WHISPERX_MODEL=large-v3 58 | - WHISPERX_LANGUAGE=ru 59 | - WHISPERX_COMPUTE_TYPE=float16 60 | - WHISPERX_BATCH_SIZE=16 61 | 62 | # Hugging Face токен (для диаризации) - используйте .env файл 63 | - HF_TOKEN=${HF_TOKEN} 64 | 65 | # Настройки суммаризации 66 | - SUMMARIZATION_API_URL=${SUMMARIZATION_API_URL:-http://localhost:11434/v1/chat/completions} 67 | - SUMMARIZATION_API_KEY=${SUMMARIZATION_API_KEY:-your-api-key-here} 68 | - SUMMARIZATION_MODEL=${SUMMARIZATION_MODEL:-llama3.1:8b} 69 | - SUMMARIZATION_MAX_TOKENS=${SUMMARIZATION_MAX_TOKENS:-4000} 70 | - SUMMARIZATION_TEMPERATURE=${SUMMARIZATION_TEMPERATURE:-0.1} 71 | 72 | # GPU настройки 73 | - CUDA_VISIBLE_DEVICES=0 74 | - NVIDIA_VISIBLE_DEVICES=all 75 | - NVIDIA_DRIVER_CAPABILITIES=compute,utility 76 | 77 | # Отладка 78 | - DEBUG=false 79 | - LOG_LEVEL=info 80 | networks: 81 | - whisperx_network 82 | command: ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8880"] 83 | healthcheck: 84 | test: ["CMD", "curl", "-f", "http://localhost:8880/health"] 85 | interval: 30s 86 | timeout: 10s 87 | retries: 3 88 | start_period: 40s 89 | # GPU поддержка (современный синтаксис) 90 | deploy: 91 | resources: 92 | reservations: 93 | devices: 94 | - driver: nvidia 95 | device_ids: ["0"] 96 | capabilities: [gpu] 97 | 98 | # WhisperX Frontend Web 99 | whisperx-frontend: 100 | build: 101 | context: . 102 | dockerfile: Dockerfile.frontend 103 | container_name: whisperx-frontend 104 | restart: unless-stopped 105 | ports: 106 | - "8000:8000" # Frontend Web 107 | volumes: 108 | # Монтирование frontend кода для разработки на лету 109 | - ./web_interface:/app/web_interface:rw 110 | environment: 111 | - HOST=0.0.0.0 112 | - PORT=8000 113 | - API_BASE_URL=http://whisperx-backend:8880 114 | networks: 115 | - whisperx_network 116 | depends_on: 117 | - whisperx-backend 118 | healthcheck: 119 | test: ["CMD", "curl", "-f", "http://localhost:8000"] 120 | interval: 30s 121 | timeout: 10s 122 | retries: 3 123 | start_period: 10s 124 | 125 | # Именованные тома для персистентного хранения 126 | volumes: 127 | whisperx_data: 128 | driver: local 129 | 130 | whisperx_models: 131 | driver: local 132 | name: whisperx_models 133 | 134 | whisperx_hf_cache: 135 | driver: local 136 | name: whisperx_hf_cache 137 | 138 | whisperx_torch_cache: 139 | driver: local 140 | name: whisperx_torch_cache 141 | 142 | # Сеть для изоляции сервисов 143 | networks: 144 | whisperx_network: 145 | driver: bridge 146 | name: whisperx_network -------------------------------------------------------------------------------- /whisperx-fronted-docker-compose-extension/README.md: -------------------------------------------------------------------------------- 1 | # 🎙️ Audio Recorder Extension 2 | 3 | Простое расширение Chrome для записи аудио с микрофона и текущей вкладки одновременно. 4 | 5 | ## ✨ Возможности 6 | 7 | - 🎤 **Запись с микрофона** - записывает ваш голос 8 | - 🖥️ **Запись аудио вкладки** - записывает звук с текущей вкладки браузера 9 | - 🔄 **Комбинированная запись** - микширует оба источника в один файл 10 | - ⏸️ **Пауза/Возобновление** - возможность приостановить и продолжить запись 11 | - 💾 **Автоматическое сохранение** - предлагает сохранить запись на рабочий стол 12 | - 🔐 **Безопасность** - запрашивает разрешения только при необходимости 13 | 14 | ## 📋 Требования 15 | 16 | - Google Chrome версии 116 или выше 17 | - Разрешение на доступ к микрофону 18 | - Активная вкладка с аудио контентом (для записи звука вкладки) 19 | 20 | ## 🚀 Установка 21 | 22 | ### Способ 1: Из исходного кода (Developer Mode) 23 | 24 | 1. **Скачайте или клонируйте репозиторий:** 25 | ```bash 26 | git clone [URL репозитория] 27 | cd audio-recorder-extension 28 | ``` 29 | 30 | 2. **Откройте Chrome и перейдите в управление расширениями:** 31 | - Введите в адресной строке: `chrome://extensions/` 32 | - Или: Меню → Дополнительные инструменты → Расширения 33 | 34 | 3. **Включите режим разработчика:** 35 | - Переключите тумблер "Режим разработчика" в правом верхнем углу 36 | 37 | 4. **Загрузите расширение:** 38 | - Нажмите "Загрузить распакованное расширение" 39 | - Выберите папку с файлами расширения 40 | 41 | 5. **Расширение установлено!** 42 | - Вы увидите иконку 🎙️ в панели расширений Chrome 43 | 44 | ## 🎯 Использование 45 | 46 | ### 1. Получение разрешения на микрофон 47 | 48 | При первом использовании: 49 | 50 | 1. Нажмите на иконку расширения 🎙️ в панели инструментов 51 | 2. В popup нажмите **"Request Microphone Permission"** 52 | 3. Откроется новая вкладка с запросом разрешения 53 | 4. Нажмите **"Allow Microphone Access"** 54 | 5. Разрешите доступ к микрофону в диалоге браузера 55 | 6. Вкладка закроется автоматически 56 | 57 | ### 2. Запись аудио 58 | 59 | 1. **Откройте вкладку** с аудио контентом (YouTube, музыка, видеозвонок и т.д.) 60 | 2. **Нажмите на иконку расширения** 🎙️ 61 | 3. **Нажмите "🎙️ Start Recording"** 62 | 4. Расширение начнет записывать: 63 | - Ваш голос с микрофона 64 | - Звук с текущей вкладки 65 | - Оба источника будут смикшированы в один файл 66 | 67 | ### 3. Управление записью 68 | 69 | - **⏸️ Pause** - приостановить запись 70 | - **▶️ Resume** - возобновить запись (кнопка изменится после паузы) 71 | - **⏹️ Stop & Save** - остановить запись и сохранить файл 72 | 73 | ### 4. Сохранение записи 74 | 75 | После остановки записи: 76 | 1. Автоматически откроется диалог сохранения файла 77 | 2. Выберите место для сохранения (по умолчанию - рабочий стол) 78 | 3. Файл будет сохранен в формате `.webm` с временной меткой 79 | 80 | ## 📁 Структура файлов 81 | 82 | ``` 83 | audio-recorder-extension/ 84 | ├── manifest.json # Конфигурация расширения 85 | ├── background.js # Service worker (фоновый скрипт) 86 | ├── popup.html # HTML интерфейса popup 87 | ├── popup.js # Логика popup интерфейса 88 | ├── permission.html # Страница запроса разрешений 89 | ├── permission.js # Логика запроса разрешений 90 | ├── offscreen.html # Offscreen документ для записи 91 | ├── offscreen.js # Логика записи аудио 92 | └── README.md # Этот файл 93 | ``` 94 | 95 | ## 🔧 Технические детали 96 | 97 | ### Используемые API: 98 | - **chrome.tabCapture** - для записи аудио вкладки 99 | - **navigator.mediaDevices.getUserMedia** - для записи с микрофона 100 | - **MediaRecorder API** - для записи аудио потоков 101 | - **Web Audio API** - для микширования аудио потоков 102 | - **chrome.offscreen** - для фоновой записи 103 | - **chrome.downloads** - для сохранения файлов 104 | 105 | ### Формат записи: 106 | - **Кодек:** Opus 107 | - **Контейнер:** WebM 108 | - **Качество:** Автоматическое (зависит от браузера) 109 | 110 | ## ❓ Решение проблем 111 | 112 | ### Проблема: "Permission dismissed" при запросе микрофона 113 | **Решение:** 114 | 1. Перейдите в настройки Chrome: `chrome://settings/content/microphone` 115 | 2. Убедитесь, что микрофон не заблокирован 116 | 3. Добавьте расширение в исключения 117 | 118 | ### Проблема: Не записывается звук вкладки 119 | **Решение:** 120 | 1. Убедитесь, что на вкладке действительно воспроизводится звук 121 | 2. Проверьте, что вкладка активна при начале записи 122 | 3. Некоторые сайты могут блокировать запись (например, Netflix) 123 | 124 | ### Проблема: Файл не сохраняется 125 | **Решение:** 126 | 1. Проверьте разрешения на запись в выбранную папку 127 | 2. Убедитесь, что достаточно места на диске 128 | 3. Попробуйте сохранить в другую папку 129 | 130 | ## 🛡️ Безопасность и конфиденциальность 131 | 132 | - Расширение запрашивает минимально необходимые разрешения 133 | - Аудио записывается только локально на вашем устройстве 134 | - Никакие данные не отправляются на внешние серверы 135 | - Разрешения запрашиваются только при необходимости 136 | 137 | ## 📝 Лицензия 138 | 139 | Этот проект предоставляется "как есть" для образовательных целей. 140 | 141 | ## 🤝 Поддержка 142 | 143 | При возникновении проблем: 144 | 1. Проверьте консоль разработчика (F12) 145 | 2. Убедитесь, что все разрешения предоставлены 146 | 3. Перезагрузите расширение в `chrome://extensions/` 147 | 148 | --- 149 | 150 | **Наслаждайтесь записью аудио! 🎵** -------------------------------------------------------------------------------- /src/middleware/auth_middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Middleware для аутентификации пользователей 3 | """ 4 | from typing import Optional 5 | from fastapi import HTTPException, status, Depends, Request, Cookie 6 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 7 | 8 | from ..services.auth_service import AuthService 9 | from ..models.schemas import User, TokenData 10 | 11 | 12 | # Создаем экземпляр HTTPBearer для извлечения токенов из заголовков 13 | security = HTTPBearer(auto_error=False) 14 | 15 | 16 | class AuthMiddleware: 17 | """Middleware для аутентификации""" 18 | 19 | def __init__(self): 20 | """Инициализация middleware аутентификации""" 21 | self.auth_service = AuthService() 22 | 23 | async def get_current_user( 24 | self, 25 | request: Request, 26 | credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), 27 | access_token: Optional[str] = Cookie(None) 28 | ) -> User: 29 | """ 30 | Получение текущего пользователя из JWT токена 31 | 32 | Args: 33 | request: HTTP запрос 34 | credentials: Токен из заголовка Authorization 35 | access_token: Токен из cookie 36 | 37 | Returns: 38 | User: Текущий пользователь 39 | 40 | Raises: 41 | HTTPException: Если пользователь не аутентифицирован 42 | """ 43 | credentials_exception = HTTPException( 44 | status_code=status.HTTP_401_UNAUTHORIZED, 45 | detail="Необходима аутентификация", 46 | headers={"WWW-Authenticate": "Bearer"}, 47 | ) 48 | 49 | # Пытаемся получить токен из заголовка или cookie 50 | token = None 51 | if credentials: 52 | token = credentials.credentials 53 | elif access_token: 54 | token = access_token 55 | 56 | if not token: 57 | raise credentials_exception 58 | 59 | # Проверяем токен 60 | token_data = self.auth_service.verify_access_token(token) 61 | if token_data is None: 62 | raise credentials_exception 63 | 64 | # Получаем пользователя 65 | user = self.auth_service.get_user_by_id(token_data.user_id) 66 | if user is None: 67 | raise credentials_exception 68 | 69 | if not user.is_active: 70 | raise HTTPException( 71 | status_code=status.HTTP_400_BAD_REQUEST, 72 | detail="Неактивный пользователь" 73 | ) 74 | 75 | return user 76 | 77 | async def get_current_user_optional( 78 | self, 79 | request: Request, 80 | credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), 81 | access_token: Optional[str] = Cookie(None) 82 | ) -> Optional[User]: 83 | """ 84 | Получение текущего пользователя (опционально) 85 | 86 | Args: 87 | request: HTTP запрос 88 | credentials: Токен из заголовка Authorization 89 | access_token: Токен из cookie 90 | 91 | Returns: 92 | Optional[User]: Текущий пользователь или None 93 | """ 94 | try: 95 | return await self.get_current_user(request, credentials, access_token) 96 | except HTTPException: 97 | return None 98 | 99 | async def get_current_active_user( 100 | self, 101 | current_user: User = Depends(lambda: auth_middleware.get_current_user) 102 | ) -> User: 103 | """ 104 | Получение текущего активного пользователя 105 | 106 | Args: 107 | current_user: Текущий пользователь 108 | 109 | Returns: 110 | User: Активный пользователь 111 | 112 | Raises: 113 | HTTPException: Если пользователь неактивен 114 | """ 115 | if not current_user.is_active: 116 | raise HTTPException( 117 | status_code=status.HTTP_400_BAD_REQUEST, 118 | detail="Неактивный пользователь" 119 | ) 120 | return current_user 121 | 122 | 123 | # Создаем глобальный экземпляр middleware 124 | auth_middleware = AuthMiddleware() 125 | 126 | 127 | # Зависимости для использования в роутах 128 | async def get_current_user( 129 | request: Request, 130 | credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), 131 | access_token: Optional[str] = Cookie(None) 132 | ) -> User: 133 | """Зависимость для получения текущего пользователя""" 134 | return await auth_middleware.get_current_user(request, credentials, access_token) 135 | 136 | 137 | async def get_current_user_optional( 138 | request: Request, 139 | credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), 140 | access_token: Optional[str] = Cookie(None) 141 | ) -> Optional[User]: 142 | """Зависимость для получения текущего пользователя (опционально)""" 143 | return await auth_middleware.get_current_user_optional(request, credentials, access_token) 144 | 145 | 146 | async def get_current_active_user( 147 | current_user: User = Depends(get_current_user) 148 | ) -> User: 149 | """Зависимость для получения текущего активного пользователя""" 150 | if not current_user.is_active: 151 | raise HTTPException( 152 | status_code=status.HTTP_400_BAD_REQUEST, 153 | detail="Неактивный пользователь" 154 | ) 155 | return current_user -------------------------------------------------------------------------------- /src/realtime/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pydantic модели для real-time транскрипции 3 | """ 4 | 5 | from typing import Optional, Dict, Any, List 6 | from pydantic import BaseModel, Field 7 | from enum import Enum 8 | import uuid 9 | from datetime import datetime 10 | 11 | 12 | class RealtimeEventType(str, Enum): 13 | """Типы событий real-time транскрипции""" 14 | SESSION_START = "session.start" 15 | SESSION_STOP = "session.stop" 16 | AUDIO_CHUNK = "audio.chunk" 17 | TRANSCRIPTION_PARTIAL = "transcription.partial" 18 | TRANSCRIPTION_FINAL = "transcription.final" 19 | ERROR = "error" 20 | STATUS = "status" 21 | 22 | 23 | class SessionConfig(BaseModel): 24 | """Конфигурация сессии транскрипции""" 25 | language: str = Field(default="ru", description="Язык транскрипции") 26 | model: str = Field(default="large-v3", description="Модель WhisperX") 27 | sample_rate: int = Field(default=24000, description="Частота дискретизации") 28 | chunk_size_ms: int = Field(default=500, description="Размер чанка в мс") 29 | buffer_size_ms: int = Field(default=5000, description="Размер буфера в мс") 30 | enable_vad: bool = Field(default=True, description="Включить Voice Activity Detection") 31 | diarization: bool = Field(default=False, description="Включить диаризацию") 32 | 33 | 34 | class RealtimeEvent(BaseModel): 35 | """Базовое событие real-time""" 36 | type: RealtimeEventType 37 | session_id: str 38 | timestamp: datetime = Field(default_factory=datetime.utcnow) 39 | data: Optional[Dict[str, Any]] = None 40 | 41 | 42 | class SessionStartEvent(RealtimeEvent): 43 | """Событие начала сессии""" 44 | type: RealtimeEventType = RealtimeEventType.SESSION_START 45 | config: SessionConfig 46 | 47 | 48 | class AudioChunkEvent(RealtimeEvent): 49 | """Событие аудио чанка""" 50 | type: RealtimeEventType = RealtimeEventType.AUDIO_CHUNK 51 | audio_data: str = Field(..., description="Base64 encoded audio data") 52 | sequence: int = Field(..., description="Порядковый номер чанка") 53 | duration_ms: int = Field(..., description="Длительность чанка в мс") 54 | 55 | 56 | class TranscriptionResult(BaseModel): 57 | """Результат транскрипции""" 58 | text: str = Field(..., description="Транскрибированный текст") 59 | confidence: float = Field(ge=0.0, le=1.0, description="Уверенность модели") 60 | start_time: Optional[float] = Field(None, description="Время начала в секундах") 61 | end_time: Optional[float] = Field(None, description="Время окончания в секундах") 62 | is_final: bool = Field(default=False, description="Финальный результат") 63 | window_info: Optional[Dict[str, Any]] = Field(None, description="Информация о скользящем окне") 64 | 65 | 66 | class TranscriptionPartialEvent(RealtimeEvent): 67 | """Событие частичной транскрипции""" 68 | type: RealtimeEventType = RealtimeEventType.TRANSCRIPTION_PARTIAL 69 | result: TranscriptionResult 70 | 71 | 72 | class TranscriptionFinalEvent(RealtimeEvent): 73 | """Событие финальной транскрипции""" 74 | type: RealtimeEventType = RealtimeEventType.TRANSCRIPTION_FINAL 75 | result: TranscriptionResult 76 | 77 | 78 | class ErrorEvent(RealtimeEvent): 79 | """Событие ошибки""" 80 | type: RealtimeEventType = RealtimeEventType.ERROR 81 | error_code: str 82 | error_message: str 83 | details: Optional[Dict[str, Any]] = None 84 | 85 | 86 | class SessionStatus(BaseModel): 87 | """Статус сессии""" 88 | session_id: str 89 | is_active: bool 90 | start_time: datetime 91 | last_activity: datetime 92 | config: SessionConfig 93 | stats: Dict[str, Any] = Field(default_factory=dict) 94 | 95 | 96 | class StatusEvent(RealtimeEvent): 97 | """Событие статуса""" 98 | type: RealtimeEventType = RealtimeEventType.STATUS 99 | status: SessionStatus 100 | 101 | 102 | # Утилиты для создания событий 103 | def create_session_id() -> str: 104 | """Создать уникальный ID сессии""" 105 | return f"rt_{uuid.uuid4().hex[:12]}" 106 | 107 | 108 | def create_transcription_partial( 109 | session_id: str, 110 | text: str, 111 | confidence: float, 112 | start_time: Optional[float] = None, 113 | end_time: Optional[float] = None 114 | ) -> TranscriptionPartialEvent: 115 | """Создать событие частичной транскрипции""" 116 | result = TranscriptionResult( 117 | text=text, 118 | confidence=confidence, 119 | start_time=start_time, 120 | end_time=end_time, 121 | is_final=False 122 | ) 123 | return TranscriptionPartialEvent(session_id=session_id, result=result) 124 | 125 | 126 | def create_transcription_final( 127 | session_id: str, 128 | text: str, 129 | confidence: float, 130 | start_time: Optional[float] = None, 131 | end_time: Optional[float] = None 132 | ) -> TranscriptionFinalEvent: 133 | """Создать событие финальной транскрипции""" 134 | result = TranscriptionResult( 135 | text=text, 136 | confidence=confidence, 137 | start_time=start_time, 138 | end_time=end_time, 139 | is_final=True 140 | ) 141 | return TranscriptionFinalEvent(session_id=session_id, result=result) 142 | 143 | 144 | def create_error_event( 145 | session_id: str, 146 | error_code: str, 147 | error_message: str, 148 | details: Optional[Dict[str, Any]] = None 149 | ) -> ErrorEvent: 150 | """Создать событие ошибки""" 151 | return ErrorEvent( 152 | session_id=session_id, 153 | error_code=error_code, 154 | error_message=error_message, 155 | details=details 156 | ) -------------------------------------------------------------------------------- /src/services/s3_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Сервис для работы с S3 (Yandex Cloud) 3 | """ 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | from pathlib import Path 7 | from typing import Optional, Dict 8 | from datetime import datetime 9 | 10 | from ..config.settings import S3_CONFIG 11 | 12 | 13 | class S3Service: 14 | """Сервис для работы с S3""" 15 | 16 | def __init__(self): 17 | self.client = self._create_client() 18 | 19 | def _create_client(self): 20 | """Создание клиента S3""" 21 | return boto3.client( 22 | 's3', 23 | aws_access_key_id=S3_CONFIG['aws_access_key_id'], 24 | aws_secret_access_key=S3_CONFIG['aws_secret_access_key'], 25 | endpoint_url=S3_CONFIG['endpoint_url'], 26 | region_name=S3_CONFIG['region_name'] 27 | ) 28 | 29 | def upload_file(self, file_path: Path, object_name: str) -> Optional[str]: 30 | """ 31 | Загрузка файла на S3 и получение публичной ссылки 32 | 33 | Args: 34 | file_path: Путь к локальному файлу 35 | object_name: Имя объекта в S3 36 | 37 | Returns: 38 | Публичная ссылка на файл или None в случае ошибки 39 | """ 40 | try: 41 | # Загружаем файл 42 | print(f"📤 Загружаем {file_path.name} на S3 как {object_name}...") 43 | self.client.upload_file(str(file_path), S3_CONFIG['bucket_name'], object_name) 44 | 45 | # Устанавливаем публичный доступ 46 | self.client.put_object_acl( 47 | ACL='public-read', 48 | Bucket=S3_CONFIG['bucket_name'], 49 | Key=object_name 50 | ) 51 | 52 | # Генерируем публичную ссылку 53 | public_url = f"{S3_CONFIG['endpoint_url']}/{S3_CONFIG['bucket_name']}/{object_name}" 54 | print(f"✅ Файл загружен на S3: {public_url}") 55 | 56 | return public_url 57 | 58 | except ClientError as e: 59 | print(f"❌ Ошибка загрузки на S3: {e}") 60 | return None 61 | except Exception as e: 62 | print(f"❌ Неожиданная ошибка при загрузке на S3: {e}") 63 | return None 64 | 65 | def upload_transcript_files(self, task_id: str, filename: str, subtitle_files: Dict[str, str]) -> Dict[str, str]: 66 | """ 67 | Загрузка файлов транскрипции на S3 68 | 69 | Args: 70 | task_id: ID задачи 71 | filename: Оригинальное имя файла 72 | subtitle_files: Словарь с путями к файлам субтитров 73 | 74 | Returns: 75 | Словарь с публичными ссылками на файлы 76 | """ 77 | s3_links = {} 78 | base_name = Path(filename).stem 79 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') 80 | 81 | for format_type, file_path in subtitle_files.items(): 82 | if not file_path or not Path(file_path).exists(): 83 | continue 84 | 85 | s3_object_name = f"transcripts/{task_id}/{base_name}_{timestamp}.{format_type}" 86 | s3_url = self.upload_file(Path(file_path), s3_object_name) 87 | 88 | if s3_url: 89 | s3_links[format_type] = s3_url 90 | 91 | return s3_links 92 | 93 | def upload_original_file(self, task_id: str, filename: str, file_path: Path) -> Optional[str]: 94 | """ 95 | Загрузка оригинального файла на S3 96 | 97 | Args: 98 | task_id: ID задачи 99 | filename: Оригинальное имя файла 100 | file_path: Путь к файлу 101 | 102 | Returns: 103 | Публичная ссылка на файл или None 104 | """ 105 | base_name = Path(filename).stem 106 | file_extension = file_path.suffix 107 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') 108 | s3_object_name = f"originals/{task_id}/{base_name}_{timestamp}{file_extension}" 109 | 110 | return self.upload_file(file_path, s3_object_name) 111 | 112 | def upload_json_data(self, task_id: str, filename: str, data: dict) -> Optional[str]: 113 | """ 114 | Загрузка JSON данных на S3 115 | 116 | Args: 117 | task_id: ID задачи 118 | filename: Оригинальное имя файла 119 | data: Данные для загрузки 120 | 121 | Returns: 122 | Публичная ссылка на файл или None 123 | """ 124 | import json 125 | from ..config.settings import TEMP_DIR 126 | 127 | # Создаем временный JSON файл 128 | temp_json_file = TEMP_DIR / f"{task_id}_full.json" 129 | 130 | try: 131 | with open(temp_json_file, 'w', encoding='utf-8') as f: 132 | json.dump(data, f, ensure_ascii=False, indent=2) 133 | 134 | # Загружаем на S3 135 | base_name = Path(filename).stem 136 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') 137 | s3_object_name = f"transcripts/{task_id}/{base_name}_{timestamp}_full.json" 138 | 139 | s3_url = self.upload_file(temp_json_file, s3_object_name) 140 | 141 | return s3_url 142 | 143 | except Exception as e: 144 | print(f"❌ Ошибка загрузки JSON на S3: {e}") 145 | return None 146 | finally: 147 | # Удаляем временный файл 148 | if temp_json_file.exists(): 149 | temp_json_file.unlink() -------------------------------------------------------------------------------- /web_interface/modules/auth.js: -------------------------------------------------------------------------------- 1 | // Модуль авторизации для Google OAuth 2 | class AuthManager { 3 | constructor() { 4 | this.apiBaseUrl = 'http://localhost:8880/api'; 5 | this.user = null; 6 | this.isAuthenticated = false; 7 | } 8 | 9 | // Проверка авторизации через API 10 | async checkAuthentication() { 11 | try { 12 | const response = await fetch(`${this.apiBaseUrl}/auth/status`, { 13 | method: 'GET', 14 | credentials: 'include', // Включаем cookies 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | } 18 | }); 19 | 20 | if (response.ok) { 21 | const data = await response.json(); 22 | if (data.authenticated) { 23 | this.isAuthenticated = true; 24 | this.user = data.user; 25 | return true; 26 | } else { 27 | this.isAuthenticated = false; 28 | this.user = null; 29 | // Перенаправляем на страницу входа 30 | window.location.href = '/login.html'; 31 | return false; 32 | } 33 | } else { 34 | this.isAuthenticated = false; 35 | this.user = null; 36 | // Перенаправляем на страницу входа 37 | window.location.href = '/login.html'; 38 | return false; 39 | } 40 | } catch (error) { 41 | console.error('Ошибка проверки аутентификации:', error); 42 | this.isAuthenticated = false; 43 | this.user = null; 44 | // Перенаправляем на страницу входа 45 | window.location.href = '/login.html'; 46 | return false; 47 | } 48 | } 49 | 50 | // Получение информации о текущем пользователе 51 | async getCurrentUser() { 52 | if (this.user) { 53 | return this.user; 54 | } 55 | 56 | try { 57 | const response = await fetch(`${this.apiBaseUrl}/auth/me`, { 58 | method: 'GET', 59 | credentials: 'include', 60 | headers: { 61 | 'Content-Type': 'application/json', 62 | } 63 | }); 64 | 65 | if (response.ok) { 66 | const userData = await response.json(); 67 | this.user = userData; 68 | this.isAuthenticated = true; 69 | return userData; 70 | } else { 71 | throw new Error('Не удалось получить данные пользователя'); 72 | } 73 | } catch (error) { 74 | console.error('Ошибка получения данных пользователя:', error); 75 | this.user = null; 76 | this.isAuthenticated = false; 77 | return null; 78 | } 79 | } 80 | 81 | // Функция выхода 82 | async logout() { 83 | try { 84 | const response = await fetch(`${this.apiBaseUrl}/auth/logout`, { 85 | method: 'POST', 86 | credentials: 'include', 87 | headers: { 88 | 'Content-Type': 'application/json', 89 | } 90 | }); 91 | 92 | // Независимо от результата, очищаем локальные данные 93 | this.user = null; 94 | this.isAuthenticated = false; 95 | 96 | // Перенаправляем на страницу входа 97 | window.location.href = '/login.html'; 98 | } catch (error) { 99 | console.error('Ошибка при выходе:', error); 100 | // Все равно очищаем и перенаправляем 101 | this.user = null; 102 | this.isAuthenticated = false; 103 | window.location.href = '/login.html'; 104 | } 105 | } 106 | 107 | // Проверка валидности сессии 108 | isSessionValid() { 109 | return this.isAuthenticated && this.user !== null; 110 | } 111 | 112 | // Получение токена из cookies (если нужно) 113 | getAuthToken() { 114 | const cookies = document.cookie.split(';'); 115 | for (let cookie of cookies) { 116 | const [name, value] = cookie.trim().split('='); 117 | if (name === 'access_token') { 118 | return value; 119 | } 120 | } 121 | return null; 122 | } 123 | 124 | // Инициализация Google OAuth входа 125 | initiateGoogleLogin() { 126 | window.location.href = `${this.apiBaseUrl}/auth/google/login`; 127 | } 128 | 129 | // Получение имени пользователя для отображения 130 | getUserDisplayName() { 131 | if (this.user) { 132 | return this.user.name || this.user.email || 'Пользователь'; 133 | } 134 | return 'Пользователь'; 135 | } 136 | 137 | // Получение email пользователя 138 | getUserEmail() { 139 | if (this.user) { 140 | return this.user.email || ''; 141 | } 142 | return ''; 143 | } 144 | 145 | // Получение аватара пользователя 146 | getUserPicture() { 147 | if (this.user && this.user.picture) { 148 | return this.user.picture; 149 | } 150 | return null; 151 | } 152 | 153 | // Проверка и инициализация аутентификации при загрузке страницы 154 | async init() { 155 | // Проверяем, не находимся ли мы на странице входа 156 | if (window.location.pathname.includes('login.html')) { 157 | return; 158 | } 159 | 160 | // Проверяем аутентификацию 161 | const isAuth = await this.checkAuthentication(); 162 | 163 | if (isAuth) { 164 | // Получаем данные пользователя 165 | await this.getCurrentUser(); 166 | } 167 | 168 | return isAuth; 169 | } 170 | } 171 | 172 | // Экспорт для использования в других модулях 173 | window.AuthManager = AuthManager; -------------------------------------------------------------------------------- /web_interface/config.js: -------------------------------------------------------------------------------- 1 | // Конфигурация AI-Transcribe Web Interface 2 | const CONFIG = { 3 | // API настройки 4 | API: { 5 | BASE_URL: 'http://localhost:8880', 6 | // HF_TOKEN передается сервером, не храним в клиентском коде 7 | ENDPOINTS: { 8 | UPLOAD: '/api/upload', 9 | STATUS: '/api/status', 10 | TRANSCRIPTIONS: '/api/transcriptions', 11 | DOWNLOAD_AUDIO: '/api/download/audio', 12 | DOWNLOAD_TRANSCRIPT: '/api/download/transcript', 13 | DOWNLOAD_SUBTITLE: '/api/download/subtitle', 14 | DELETE_TRANSCRIPTION: '/api/transcription', 15 | S3_LINKS: '/api/s3-links', // Новый эндпоинт для получения S3 ссылок 16 | SUMMARIZE: '/api/summarize', // Эндпоинт для суммаризации 17 | SUMMARIZATION_CONFIG: '/api/config/summarization' // Эндпоинт для конфигурации суммаризации 18 | } 19 | }, 20 | 21 | // Настройки авторизации 22 | AUTH: { 23 | PASSWORD: 'AI-Transcribe', 24 | SESSION_DURATION: 24 * 60 * 60 * 1000, // 24 часа в миллисекундах 25 | STORAGE_KEYS: { 26 | IS_LOGGED_IN: 'isLoggedIn', 27 | LOGIN_TIME: 'loginTime' 28 | } 29 | }, 30 | 31 | // Настройки интерфейса 32 | UI: { 33 | PROGRESS_UPDATE_INTERVAL: 2000, // Интервал обновления прогресса в мс 34 | NOTIFICATION_DURATION: 3000, // Длительность показа уведомлений в мс 35 | INFO_NOTIFICATION_DURATION: 5000, // Длительность показа информационных уведомлений в мс 36 | MEDIA_LOADING_TIMEOUT: 30000, // Таймаут загрузки медиа в мс 37 | AUTO_SCROLL: true, // Автопрокрутка транскрипта по умолчанию 38 | SHOW_TIMESTAMPS: true // Показывать временные метки по умолчанию 39 | }, 40 | 41 | // Настройки транскрипции по умолчанию 42 | TRANSCRIPTION: { 43 | DEFAULT_MODEL: 'large-v3', 44 | DEFAULT_LANGUAGE: 'ru', 45 | DEFAULT_DIARIZE: false, 46 | DEFAULT_COMPUTE_TYPE: 'auto', 47 | DEFAULT_BATCH_SIZE: 16, 48 | MODELS: [ 49 | { value: 'large-v3', label: 'Large-v3 (лучшее качество)' }, 50 | { value: 'medium', label: 'Medium (быстрее)' }, 51 | { value: 'small', label: 'Small (самый быстрый)' } 52 | ], 53 | LANGUAGES: [ 54 | { value: 'ru', label: 'Русский' }, 55 | { value: 'en', label: 'English' }, 56 | { value: 'auto', label: 'Автоопределение' } 57 | ] 58 | }, 59 | 60 | // Настройки суммаризации перенесены в переменные окружения сервера 61 | // Конфигурация теперь получается через API: /api/config/summarization 62 | 63 | // Поддерживаемые форматы файлов 64 | FILE_FORMATS: { 65 | AUDIO: ['mp3', 'm4a', 'wav', 'flac', 'ogg', 'wma', 'aac', 'opus'], 66 | VIDEO: ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm', '3gp', 'mts'], 67 | EXPORT: ['json', 'srt', 'vtt', 'tsv', 'docx', 'pdf'] 68 | }, 69 | 70 | // Настройки экспорта 71 | EXPORT: { 72 | FORMATS: { 73 | json: { label: 'JSON', icon: 'fas fa-download' }, 74 | srt: { label: 'SRT', icon: 'fas fa-download' }, 75 | vtt: { label: 'VTT', icon: 'fas fa-download' }, 76 | tsv: { label: 'TSV', icon: 'fas fa-download' }, 77 | docx: { label: 'DOCX', icon: 'fas fa-file-word' }, 78 | pdf: { label: 'PDF', icon: 'fas fa-file-pdf' } 79 | } 80 | }, 81 | 82 | // Цвета для спикеров 83 | SPEAKER_COLORS: [ 84 | '#4CAF50', // Зеленый 85 | '#FF9800', // Оранжевый 86 | '#9C27B0', // Фиолетовый 87 | '#2196F3', // Синий 88 | '#F44336', // Красный 89 | '#795548', // Коричневый 90 | '#607D8B', // Серо-синий 91 | '#E91E63' // Розовый 92 | ], 93 | 94 | // Сообщения интерфейса 95 | MESSAGES: { 96 | ERRORS: { 97 | NO_FILE_SELECTED: 'Пожалуйста, выберите файл для транскрипции', 98 | FILE_UPLOAD_ERROR: 'Ошибка загрузки файла', 99 | TRANSCRIPTION_NOT_COMPLETED: 'Транскрипция не завершена или повреждена', 100 | DOWNLOAD_ERROR: 'Ошибка скачивания файла', 101 | NETWORK_ERROR: 'Ошибка подключения к серверу. Проверьте интернет-соединение.', 102 | FILE_NOT_FOUND: 'Файл не найден на сервере.', 103 | DELETE_ERROR: 'Ошибка удаления транскрипции', 104 | LOAD_HISTORY_ERROR: 'Ошибка загрузки истории', 105 | LOAD_TRANSCRIPTION_ERROR: 'Ошибка загрузки транскрипции' 106 | }, 107 | SUCCESS: { 108 | TRANSCRIPTION_DELETED: 'Транскрипция успешно удалена', 109 | LOGIN_SUCCESS: 'Вход выполнен успешно! Перенаправление...', 110 | FILE_DOWNLOADED: 'Готово' 111 | }, 112 | INFO: { 113 | TRANSCRIPTION_CANCELLED: 'Транскрипция отменена', 114 | MEDIA_LOADING_SKIPPED: 'Загрузка медиа пропущена. Показан только транскрипт.', 115 | MEDIA_LOAD_ERROR: 'Медиа файл не удалось загрузить, но транскрипт доступен для просмотра', 116 | LARGE_FILE_WARNING: 'Большие файлы могут загружаться несколько минут...' 117 | }, 118 | PROGRESS: { 119 | PREPARING: 'Подготовка к обработке...', 120 | LOADING_MODELS: 'Загрузка моделей...', 121 | LOADING_AUDIO: 'Загрузка аудио...', 122 | TRANSCRIBING: 'Выполнение транскрипции...', 123 | ALIGNING: 'Выравнивание текста...', 124 | DIARIZING: 'Диаризация спикеров...', 125 | SAVING: 'Сохранение результатов...', 126 | PROCESSING: 'Обработка...' 127 | } 128 | }, 129 | 130 | // Настройки разработки 131 | DEBUG: { 132 | ENABLED: true, // Включить отладочные сообщения 133 | LOG_API_CALLS: true, // Логировать API вызовы 134 | SIMULATE_SLOW_NETWORK: false // Симулировать медленную сеть 135 | } 136 | }; 137 | 138 | // Экспортируем конфигурацию для использования в других файлах 139 | if (typeof module !== 'undefined' && module.exports) { 140 | module.exports = CONFIG; 141 | } -------------------------------------------------------------------------------- /web_interface/modules/fileHandler.js: -------------------------------------------------------------------------------- 1 | // Модуль для обработки файлов 2 | class FileHandler { 3 | constructor() { 4 | this.selectedFile = null; 5 | this.onFileSelected = null; // callback функция 6 | } 7 | 8 | // Инициализация обработчиков событий 9 | initializeEventListeners() { 10 | const fileInput = document.getElementById('fileInput'); 11 | const uploadArea = document.getElementById('uploadArea'); 12 | const selectFileBtn = document.getElementById('selectFileBtn'); 13 | 14 | if (fileInput) { 15 | fileInput.addEventListener('change', (event) => this.handleFileSelect(event)); 16 | } 17 | 18 | if (selectFileBtn) { 19 | selectFileBtn.addEventListener('click', (event) => { 20 | event.stopPropagation(); // Предотвращаем всплытие события 21 | fileInput?.click(); 22 | }); 23 | } 24 | 25 | if (uploadArea) { 26 | uploadArea.addEventListener('click', (event) => { 27 | // Проверяем, что клик был не по кнопке 28 | if (!event.target.closest('#selectFileBtn') && !event.target.closest('button')) { 29 | event.stopPropagation(); // Предотвращаем всплытие события 30 | fileInput?.click(); 31 | } 32 | }); 33 | uploadArea.addEventListener('dragover', (event) => this.handleDragOver(event)); 34 | uploadArea.addEventListener('dragleave', (event) => this.handleDragLeave(event)); 35 | uploadArea.addEventListener('drop', (event) => this.handleFileDrop(event)); 36 | } 37 | } 38 | 39 | // Обработка выбора файла 40 | handleFileSelect(event) { 41 | const file = event.target.files[0]; 42 | if (file) { 43 | // Предотвращаем повторную обработку того же файла 44 | if (this.selectedFile && this.selectedFile.name === file.name && 45 | this.selectedFile.size === file.size && 46 | this.selectedFile.lastModified === file.lastModified) { 47 | console.log('Файл уже выбран, пропускаем повторную обработку'); 48 | return; 49 | } 50 | this.setSelectedFile(file); 51 | } 52 | } 53 | 54 | // Обработка перетаскивания файлов 55 | handleDragOver(event) { 56 | event.preventDefault(); 57 | event.currentTarget.classList.add('dragover'); 58 | } 59 | 60 | handleDragLeave(event) { 61 | event.currentTarget.classList.remove('dragover'); 62 | } 63 | 64 | handleFileDrop(event) { 65 | event.preventDefault(); 66 | event.currentTarget.classList.remove('dragover'); 67 | 68 | const files = event.dataTransfer.files; 69 | if (files.length > 0) { 70 | const file = files[0]; 71 | const fileInput = document.getElementById('fileInput'); 72 | if (fileInput) { 73 | fileInput.files = files; 74 | } 75 | this.setSelectedFile(file); 76 | } 77 | } 78 | 79 | // Установка выбранного файла 80 | setSelectedFile(file) { 81 | this.selectedFile = file; 82 | this.showFileSelected(file); 83 | 84 | // Вызываем callback если он установлен 85 | if (this.onFileSelected && typeof this.onFileSelected === 'function') { 86 | this.onFileSelected(file); 87 | } 88 | } 89 | 90 | // Показать выбранный файл 91 | showFileSelected(file) { 92 | const settingsPanel = document.getElementById('settingsPanel'); 93 | if (settingsPanel) { 94 | settingsPanel.style.display = 'block'; 95 | } 96 | 97 | // Обновить информацию о файле 98 | const uploadContent = document.querySelector('.upload-content h3'); 99 | if (uploadContent) { 100 | uploadContent.textContent = `Выбран файл: ${file.name}`; 101 | } 102 | 103 | // Показать размер файла 104 | const fileSize = (file.size / (1024 * 1024)).toFixed(2); 105 | const uploadDesc = document.querySelector('.upload-content p'); 106 | if (uploadDesc) { 107 | uploadDesc.innerHTML = `Размер: ${fileSize} МБ | Тип: ${file.type}
108 | Поддерживаются: MP3, M4A, WAV, MP4, AVI, MKV и другие
109 | Экспорт в 6 форматов: JSON, SRT, VTT, TSV, DOCX, PDF
110 | ☁️ Автоматическая загрузка на Yandex Cloud S3`; 111 | } 112 | } 113 | 114 | // Получить выбранный файл 115 | getSelectedFile() { 116 | return this.selectedFile; 117 | } 118 | 119 | // Проверка типа файла 120 | isValidFileType(file) { 121 | const audioFormats = CONFIG.FILE_FORMATS.AUDIO; 122 | const videoFormats = CONFIG.FILE_FORMATS.VIDEO; 123 | const allFormats = [...audioFormats, ...videoFormats]; 124 | 125 | const fileExtension = file.name.split('.').pop().toLowerCase(); 126 | return allFormats.includes(fileExtension); 127 | } 128 | 129 | // Получение типа медиа (audio/video) 130 | getMediaType(file) { 131 | const audioFormats = CONFIG.FILE_FORMATS.AUDIO; 132 | const fileExtension = file.name.split('.').pop().toLowerCase(); 133 | 134 | return audioFormats.includes(fileExtension) ? 'audio' : 'video'; 135 | } 136 | 137 | // Очистка выбранного файла 138 | clearSelectedFile() { 139 | this.selectedFile = null; 140 | const fileInput = document.getElementById('fileInput'); 141 | if (fileInput) { 142 | fileInput.value = ''; 143 | } 144 | 145 | const settingsPanel = document.getElementById('settingsPanel'); 146 | if (settingsPanel) { 147 | settingsPanel.style.display = 'none'; 148 | } 149 | 150 | // Восстановить исходный текст 151 | const uploadContent = document.querySelector('.upload-content h3'); 152 | if (uploadContent) { 153 | uploadContent.textContent = 'Перетащите файл сюда или нажмите для выбора'; 154 | } 155 | 156 | const uploadDesc = document.querySelector('.upload-content p'); 157 | if (uploadDesc) { 158 | uploadDesc.innerHTML = `Поддерживаются: MP3, M4A, WAV, MP4, AVI, MKV и другие
159 | Экспорт в 6 форматов: JSON, SRT, VTT, TSV, DOCX, PDF
160 | ☁️ Автоматическая загрузка на Yandex Cloud S3`; 161 | } 162 | } 163 | 164 | // Установка callback функции для обработки выбора файла 165 | setOnFileSelectedCallback(callback) { 166 | this.onFileSelected = callback; 167 | } 168 | } 169 | 170 | // Экспорт для использования в других модулях 171 | window.FileHandler = FileHandler; -------------------------------------------------------------------------------- /DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # 🚀 Руководство по развертыванию whisperx-fronted-docker-compose 2 | 3 | > **Основано на [WhisperX](https://github.com/m-bain/whisperX)** by Max Bain 4 | > Лицензия BSD-2-Clause 5 | 6 | ## 📋 Предварительные требования 7 | 8 | ### Системные требования 9 | - **Python 3.8+** с pip 10 | - **Node.js 16+** (для фронтенда) 11 | - **Docker** и **Docker Compose** (опционально) 12 | - **CUDA-совместимая GPU** (рекомендуется для лучшей производительности) 13 | 14 | ### Внешние сервисы 15 | - **Yandex Cloud S3** аккаунт для хранения файлов 16 | - **Google OAuth 2.0** приложение для аутентификации 17 | - **HuggingFace** токен для диаризации спикеров 18 | - **Ollama** или другой LLM API для суммаризации (опционально) 19 | 20 | ## ⚙️ Настройка переменных окружения 21 | 22 | ### 1. Создание файла конфигурации 23 | ```bash 24 | # Скопируйте пример конфигурации 25 | cp .env.example .env 26 | ``` 27 | 28 | ### 2. Настройка основных параметров 29 | ```bash 30 | # === ОСНОВНЫЕ НАСТРОЙКИ === 31 | ENVIRONMENT=production 32 | DEBUG=false 33 | LOG_LEVEL=info 34 | 35 | # === НАСТРОЙКИ СЕРВЕРА === 36 | HOST=0.0.0.0 37 | BACKEND_PORT=8880 38 | FRONTEND_PORT=8000 39 | FRONTEND_URL=https://your-domain.com 40 | BACKEND_URL=https://api.your-domain.com 41 | ``` 42 | 43 | ### 3. Настройка Yandex Cloud S3 44 | ```bash 45 | # Получите ключи в консоли Yandex Cloud 46 | S3_ACCESS_KEY=your_s3_access_key 47 | S3_SECRET_KEY=your_s3_secret_key 48 | S3_BUCKET=your_s3_bucket_name 49 | S3_ENDPOINT=https://storage.yandexcloud.net 50 | S3_REGION=ru-central1 51 | ``` 52 | 53 | ### 4. Настройка Google OAuth 54 | ```bash 55 | # Создайте OAuth приложение в Google Cloud Console 56 | GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com 57 | GOOGLE_CLIENT_SECRET=your_google_client_secret 58 | REDIRECT_URI=https://api.your-domain.com/api/auth/oauth/google/callback 59 | ``` 60 | 61 | ### 5. Настройка JWT и безопасности 62 | ```bash 63 | # Сгенерируйте случайный ключ 64 | JWT_SECRET_KEY=$(openssl rand -hex 32) 65 | ``` 66 | 67 | ### 6. Настройка HuggingFace 68 | ```bash 69 | # Получите токен на https://huggingface.co/settings/tokens 70 | HF_TOKEN=your_huggingface_token 71 | ``` 72 | 73 | ## 🐳 Развертывание с Docker 74 | 75 | ### 1. Сборка и запуск 76 | ```bash 77 | # Сборка образов 78 | docker-compose build 79 | 80 | # Запуск сервисов 81 | docker-compose up -d 82 | 83 | # Проверка статуса 84 | docker-compose ps 85 | ``` 86 | 87 | ### 2. Мониторинг логов 88 | ```bash 89 | # Просмотр логов 90 | docker-compose logs -f 91 | 92 | # Логи конкретного сервиса 93 | docker-compose logs -f backend 94 | docker-compose logs -f frontend 95 | ``` 96 | 97 | ## 🔧 Ручное развертывание 98 | 99 | ### 1. Установка зависимостей 100 | ```bash 101 | # Python зависимости 102 | pip install -r requirements.txt 103 | 104 | # Node.js зависимости (если используется) 105 | cd web_interface && npm install 106 | ``` 107 | 108 | ### 2. Запуск backend 109 | ```bash 110 | # Из корневой директории 111 | python src/main.py 112 | ``` 113 | 114 | ### 3. Запуск frontend 115 | ```bash 116 | # Статический сервер 117 | cd web_interface && python server.py 118 | ``` 119 | 120 | ## 🌐 Настройка веб-интерфейса 121 | 122 | ### 1. Обновление конфигурации 123 | ```bash 124 | # Скопируйте пример 125 | cp web_interface/config.example.js web_interface/config.js 126 | ``` 127 | 128 | ### 2. Настройка API endpoints 129 | ```javascript 130 | const CONFIG = { 131 | API: { 132 | BASE_URL: 'https://api.your-domain.com', 133 | // ... другие настройки 134 | } 135 | 136 | // Настройки суммаризации теперь получаются с сервера 137 | // через API endpoint /api/config/summarization 138 | // Настройте переменные окружения в .env файле: 139 | // SUMMARIZATION_API_URL, SUMMARIZATION_API_KEY, SUMMARIZATION_MODEL, и т.д. 140 | }; 141 | ``` 142 | 143 | ## 🔌 Настройка Chrome расширения 144 | 145 | ### 1. Обновление конфигурации 146 | ```javascript 147 | // whisperx-fronted-docker-compose-extension/config.js 148 | const CONFIG = { 149 | API_BASE: 'https://api.your-domain.com/api', 150 | FRONTEND_URL: 'https://your-domain.com', 151 | 152 | PERMISSIONS: [ 153 | 'https://api.your-domain.com/*', 154 | 'https://your-domain.com/*' 155 | ] 156 | }; 157 | ``` 158 | 159 | ### 2. Обновление манифеста 160 | ```json 161 | { 162 | "permissions": [ 163 | "https://api.your-domain.com/*", 164 | "https://your-domain.com/*" 165 | ] 166 | } 167 | ``` 168 | 169 | ## 🔒 Настройка безопасности 170 | 171 | ### 1. Настройка HTTPS 172 | ```bash 173 | # Получите SSL сертификаты (Let's Encrypt) 174 | certbot certonly --webroot -w /var/www/html -d your-domain.com 175 | ``` 176 | 177 | ### 2. Настройка CORS 178 | ```python 179 | # В src/main.py добавьте ваши домены 180 | origins = [ 181 | "https://your-domain.com", 182 | "https://api.your-domain.com" 183 | ] 184 | ``` 185 | 186 | ### 3. Настройка firewall 187 | ```bash 188 | # Откройте только необходимые порты 189 | ufw allow 80/tcp 190 | ufw allow 443/tcp 191 | ufw allow 8880/tcp # API порт 192 | ``` 193 | 194 | ## 📊 Мониторинг и логирование 195 | 196 | ### 1. Настройка логов 197 | ```bash 198 | # Создайте директорию для логов 199 | mkdir -p /var/log/whisperx-fronted-docker-compose 200 | 201 | # Настройте ротацию логов 202 | sudo logrotate -d /etc/logrotate.d/whisperx-fronted-docker-compose 203 | ``` 204 | 205 | ### 2. Мониторинг производительности 206 | ```bash 207 | # Установите мониторинг 208 | pip install prometheus-client 209 | ``` 210 | 211 | ## 🔄 Обновление системы 212 | 213 | ### 1. Бэкап данных 214 | ```bash 215 | # Создайте бэкап базы данных 216 | cp data/transcriptions_db.json data/transcriptions_db.json.backup 217 | 218 | # Бэкап конфигурации 219 | cp .env .env.backup 220 | ``` 221 | 222 | ### 2. Обновление кода 223 | ```bash 224 | # Получите последнюю версию 225 | git pull origin main 226 | 227 | # Обновите зависимости 228 | pip install -r requirements.txt 229 | 230 | # Перезапустите сервисы 231 | docker-compose restart 232 | ``` 233 | 234 | ## 🆘 Устранение неполадок 235 | 236 | ### Проблемы с GPU 237 | ```bash 238 | # Проверьте CUDA 239 | nvidia-smi 240 | 241 | # Проверьте PyTorch 242 | python -c "import torch; print(torch.cuda.is_available())" 243 | ``` 244 | 245 | ### Проблемы с памятью 246 | ```bash 247 | # Уменьшите batch_size в .env 248 | WHISPERX_BATCH_SIZE=8 249 | 250 | # Используйте меньшую модель 251 | WHISPERX_MODEL=medium 252 | ``` 253 | 254 | ### Проблемы с сетью 255 | ```bash 256 | # Проверьте подключение к S3 257 | aws s3 ls --endpoint-url=https://storage.yandexcloud.net 258 | 259 | # Проверьте API endpoints 260 | curl -X GET https://api.your-domain.com/api/health 261 | ``` 262 | 263 | ## 📞 Поддержка 264 | 265 | Если у вас возникли проблемы: 266 | 1. Проверьте логи: `docker-compose logs -f` 267 | 2. Убедитесь, что все переменные окружения настроены 268 | 3. Проверьте сетевую связность 269 | 4. Создайте issue в GitHub репозитории 270 | 271 | --- 272 | 273 | **Важно**: Никогда не коммитьте файл `.env` в Git! Все секретные ключи должны храниться в переменных окружения. -------------------------------------------------------------------------------- /src/api/auth_routes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Роуты для аутентификации через Google OAuth 3 | """ 4 | import os 5 | import uuid 6 | from typing import Optional 7 | from fastapi import APIRouter, Request, HTTPException, status, Depends, Response, Cookie 8 | from fastapi.responses import RedirectResponse, JSONResponse 9 | from starlette.requests import Request 10 | 11 | from ..services.auth_service import AuthService 12 | from ..models.schemas import AuthResponse, User 13 | from ..middleware.auth_middleware import get_current_user, get_current_user_optional 14 | 15 | 16 | router = APIRouter() 17 | auth_service = AuthService() 18 | 19 | 20 | @router.get("/auth/google/login") 21 | async def google_login(request: Request): 22 | """ 23 | Инициация процесса аутентификации через Google 24 | 25 | Returns: 26 | RedirectResponse: Перенаправление на страницу аутентификации Google 27 | """ 28 | try: 29 | # Генерируем состояние для защиты от CSRF 30 | state = str(uuid.uuid4()) 31 | 32 | # Сохраняем состояние в сессии 33 | request.session['oauth_state'] = state 34 | 35 | # Получаем URL для аутентификации 36 | auth_url = auth_service.get_google_auth_url(state) 37 | 38 | return RedirectResponse(url=auth_url) 39 | 40 | except Exception as e: 41 | raise HTTPException( 42 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 43 | detail=f"Ошибка при инициации аутентификации: {str(e)}" 44 | ) 45 | 46 | 47 | @router.get("/auth/oauth/google/callback") 48 | async def google_callback( 49 | request: Request, 50 | code: Optional[str] = None, 51 | state: Optional[str] = None, 52 | error: Optional[str] = None 53 | ): 54 | """ 55 | Обработка callback от Google OAuth 56 | 57 | Args: 58 | request: HTTP запрос 59 | code: Код авторизации от Google 60 | state: Состояние для защиты от CSRF 61 | error: Ошибка от Google 62 | 63 | Returns: 64 | RedirectResponse: Перенаправление на фронтенд с токеном 65 | """ 66 | try: 67 | # Проверяем на ошибки от Google 68 | if error: 69 | raise HTTPException( 70 | status_code=status.HTTP_400_BAD_REQUEST, 71 | detail=f"Ошибка аутентификации Google: {error}" 72 | ) 73 | 74 | # Проверяем наличие кода 75 | if not code: 76 | raise HTTPException( 77 | status_code=status.HTTP_400_BAD_REQUEST, 78 | detail="Отсутствует код авторизации" 79 | ) 80 | 81 | # Проверяем состояние для защиты от CSRF 82 | session_state = request.session.get('oauth_state') 83 | if not session_state or session_state != state: 84 | raise HTTPException( 85 | status_code=status.HTTP_400_BAD_REQUEST, 86 | detail="Неверное состояние OAuth" 87 | ) 88 | 89 | # Обмениваем код на токен 90 | id_token = auth_service.exchange_code_for_token(code) 91 | if not id_token: 92 | raise HTTPException( 93 | status_code=status.HTTP_400_BAD_REQUEST, 94 | detail="Не удалось получить токен от Google" 95 | ) 96 | 97 | # Проверяем Google токен 98 | google_user = auth_service.verify_google_token(id_token) 99 | if not google_user: 100 | raise HTTPException( 101 | status_code=status.HTTP_400_BAD_REQUEST, 102 | detail="Неверный токен Google" 103 | ) 104 | 105 | # Создаем или обновляем пользователя 106 | user = auth_service.create_or_update_user(google_user) 107 | 108 | # Создаем JWT токен 109 | access_token = auth_service.create_access_token( 110 | data={"sub": user.id, "email": user.email} 111 | ) 112 | 113 | # Создаем сессию пользователя 114 | user_agent = request.headers.get("User-Agent") 115 | client_ip = request.client.host if request.client else None 116 | auth_service.create_user_session(user.id, user_agent, client_ip) 117 | 118 | # Очищаем состояние из сессии 119 | request.session.pop('oauth_state', None) 120 | 121 | # Создаем ответ с перенаправлением на фронтенд 122 | frontend_url = os.getenv('FRONTEND_URL', 'http://localhost:8000') 123 | response = RedirectResponse(url=f"{frontend_url}?auth=success") 124 | 125 | # Устанавливаем cookie с токеном 126 | response.set_cookie( 127 | key="access_token", 128 | value=access_token, 129 | max_age=60 * 60 * 24 * 7, # 7 дней 130 | httponly=True, 131 | secure=False, # В продакшене должно быть True для HTTPS 132 | samesite="lax" 133 | ) 134 | 135 | return response 136 | 137 | except HTTPException: 138 | raise 139 | except Exception as e: 140 | raise HTTPException( 141 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 142 | detail=f"Ошибка при обработке callback: {str(e)}" 143 | ) 144 | 145 | 146 | @router.post("/auth/logout") 147 | async def logout( 148 | response: Response, 149 | current_user: User = Depends(get_current_user) 150 | ): 151 | """ 152 | Выход из системы 153 | 154 | Args: 155 | response: HTTP ответ 156 | current_user: Текущий пользователь 157 | 158 | Returns: 159 | dict: Сообщение об успешном выходе 160 | """ 161 | try: 162 | # Удаляем все сессии пользователя 163 | auth_service.db_service.delete_user_sessions(current_user.id) 164 | 165 | # Удаляем cookie с токеном 166 | response.delete_cookie(key="access_token") 167 | 168 | return {"message": "Успешный выход из системы"} 169 | 170 | except Exception as e: 171 | raise HTTPException( 172 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 173 | detail=f"Ошибка при выходе из системы: {str(e)}" 174 | ) 175 | 176 | 177 | @router.get("/auth/me") 178 | async def get_current_user_info( 179 | current_user: User = Depends(get_current_user) 180 | ): 181 | """ 182 | Получение информации о текущем пользователе 183 | 184 | Args: 185 | current_user: Текущий пользователь 186 | 187 | Returns: 188 | User: Информация о пользователе 189 | """ 190 | return current_user 191 | 192 | 193 | @router.get("/auth/status") 194 | async def auth_status( 195 | current_user: Optional[User] = Depends(get_current_user_optional) 196 | ): 197 | """ 198 | Проверка статуса аутентификации 199 | 200 | Args: 201 | current_user: Текущий пользователь (опционально) 202 | 203 | Returns: 204 | dict: Статус аутентификации 205 | """ 206 | if current_user: 207 | return { 208 | "authenticated": True, 209 | "user": { 210 | "id": current_user.id, 211 | "email": current_user.email, 212 | "name": current_user.name, 213 | "picture": current_user.picture 214 | } 215 | } 216 | else: 217 | return { 218 | "authenticated": False, 219 | "user": None 220 | } -------------------------------------------------------------------------------- /cursor-to-do/EXTENSION_DEVELOPMENT_PLAN.md: -------------------------------------------------------------------------------- 1 | # 🎤 whisperx-fronted-docker-compose Browser Extension - Упрощенный флоу 2 | 3 | ## 📋 Обзор проекта 4 | 5 | **Цель**: Максимально простое расширение для записи встреч с авторизацией. 6 | 7 | **Реальный API**: `https://redmadtranscribe-api.neuraldeep.tech/` 8 | 9 | **Упрощенный флоу**: 10 | 1. 🔐 **Пользователь открывает popup** → авторизуется → токен в сессию 11 | 2. 🎧 **Записывает встречу** → нажимает "Отправить" 12 | 3. 📤 **Получает уведомление** → "Запись отправлена, перейдите в интерфейс" 13 | 14 | --- 15 | 16 | ## 🚀 Простой флоу пользователя 17 | 18 | ### 1. Открытие расширения 19 | ``` 20 | Пользователь кликает на иконку расширения 21 | ↓ 22 | Открывается popup 23 | ↓ 24 | Проверяется авторизация (есть ли токен в сессии) 25 | ``` 26 | 27 | ### 2. Авторизация (если нужна) 28 | ``` 29 | Если токена нет: 30 | ↓ 31 | Показывается кнопка "Войти через Google" 32 | ↓ 33 | Клик → открывается веб-интерфейс OAuth 34 | ↓ 35 | После успешного входа → токен сохраняется в сессию браузера 36 | ↓ 37 | Пользователь возвращается к расширению 38 | ``` 39 | 40 | ### 3. Запись и отправка 41 | ``` 42 | Токен есть → показывается кнопка "Записать встречу" 43 | ↓ 44 | Клик → начинается запись аудио вкладки 45 | ↓ 46 | Пользователь видит таймер записи 47 | ↓ 48 | Клик "Остановить" → запись готова 49 | ↓ 50 | Автоматическая отправка на API с токеном авторизации 51 | ↓ 52 | Уведомление: "Запись отправлена! Перейдите в веб-интерфейс для работы" 53 | ``` 54 | 55 | --- 56 | 57 | ## 🔐 Техническая реализация авторизации 58 | 59 | ### Проверка токена в сессии 60 | ```javascript 61 | // При открытии popup проверяем авторизацию 62 | async function checkAuth() { 63 | try { 64 | const response = await fetch('https://redmadtranscribe-api.neuraldeep.tech/api/auth/status', { 65 | credentials: 'include' // Используем cookie из сессии браузера 66 | }); 67 | 68 | const data = await response.json(); 69 | 70 | if (data.authenticated && data.user) { 71 | // Токен есть, пользователь авторизован 72 | showRecordingInterface(data.user); 73 | } else { 74 | // Токена нет, нужна авторизация 75 | showLoginInterface(); 76 | } 77 | } catch (error) { 78 | // Ошибка - показываем вход 79 | showLoginInterface(); 80 | } 81 | } 82 | ``` 83 | 84 | ### Авторизация через веб-интерфейс 85 | ```javascript 86 | // Простой вход через существующий веб-интерфейс 87 | function handleLogin() { 88 | // Открываем страницу авторизации в новой вкладке 89 | chrome.tabs.create({ 90 | url: 'https://redmadtranscribe.neuraldeep.tech/login.html' 91 | }); 92 | 93 | // Закрываем popup - пользователь вернется после входа 94 | window.close(); 95 | } 96 | 97 | // После входа пользователь снова открывает расширение 98 | // Токен уже будет в сессии браузера (HTTP-only cookie) 99 | ``` 100 | 101 | ### Отправка записи с авторизацией 102 | ```javascript 103 | // Отправляем запись используя токен из сессии 104 | async function uploadRecording(audioBlob, userInfo) { 105 | const formData = new FormData(); 106 | formData.append('file', audioBlob, `meeting-${userInfo.email}-${Date.now()}.webm`); 107 | formData.append('diarize', 'true'); 108 | formData.append('language', 'auto'); 109 | 110 | const response = await fetch('https://redmadtranscribe-api.neuraldeep.tech/api/upload', { 111 | method: 'POST', 112 | body: formData, 113 | credentials: 'include' // Автоматически отправляет токен из cookie 114 | }); 115 | 116 | if (response.ok) { 117 | // Успех - показываем уведомление 118 | showSuccessNotification(); 119 | } else if (response.status === 401) { 120 | // Токен истек - нужна повторная авторизация 121 | showLoginInterface(); 122 | } 123 | } 124 | ``` 125 | 126 | --- 127 | 128 | ## 📱 Упрощенный UI 129 | 130 | ### Состояние 1: Нужна авторизация 131 | ```html 132 |
133 |

🎤 whisperx-fronted-docker-compose

134 |

Войдите для записи встреч

135 | 136 |
137 | ``` 138 | 139 | ### Состояние 2: Готов к записи 140 | ```html 141 |
142 |

🎤 whisperx-fronted-docker-compose

143 |
Привет, Имя!
144 | 145 | 146 | 147 | 148 | 149 |
Готов к записи
150 |
151 | ``` 152 | 153 | ### Состояние 3: Отправка завершена 154 | ```html 155 |
156 |

✅ Готово!

157 |

Запись отправлена на обработку

158 | 159 |
160 | ``` 161 | 162 | --- 163 | 164 | ## 🎯 Ключевые упрощения 165 | 166 | ### 1. Авторизация через браузер 167 | - ✅ **Используем существующий OAuth** - никаких новых механизмов 168 | - ✅ **Токен в HTTP-only cookie** - автоматически безопасно 169 | - ✅ **Общая сессия** - один вход для веб и расширения 170 | - ✅ **Простая проверка** - один API вызов `/auth/status` 171 | 172 | ### 2. Минимальный UI 173 | - ✅ **2 основных состояния** - вход или запись 174 | - ✅ **Автоматические переходы** - после входа сразу готов к записи 175 | - ✅ **Простые кнопки** - "Войти", "Записать", "Остановить" 176 | - ✅ **Понятные статусы** - что происходит в каждый момент 177 | 178 | ### 3. Автоматическая отправка 179 | - ✅ **Без лишних кликов** - запись автоматически отправляется после остановки 180 | - ✅ **Уведомление с действием** - клик открывает веб-интерфейс 181 | - ✅ **Обработка ошибок** - если токен истек, предлагаем войти заново 182 | 183 | --- 184 | 185 | ## 🔄 Полный цикл использования 186 | 187 | ``` 188 | 1. Пользователь на встрече (Zoom/Meet/Teams) 189 | ↓ 190 | 2. Кликает иконку расширения 191 | ↓ 192 | 3. Если не авторизован → "Войти через Google" → OAuth → токен в сессии 193 | ↓ 194 | 4. Если авторизован → "Записать встречу" → запись начинается 195 | ↓ 196 | 5. Видит таймер, кликает "Остановить и отправить" 197 | ↓ 198 | 6. Автоматическая загрузка на сервер с токеном 199 | ↓ 200 | 7. Уведомление: "Запись отправлена! Перейдите в веб-интерфейс" 201 | ↓ 202 | 8. Клик на уведомление → открывается веб-интерфейс с результатами 203 | ``` 204 | 205 | **Время использования**: 30 секунд на авторизацию (первый раз) + 10 секунд на запуск записи + автоматическая отправка 206 | 207 | --- 208 | 209 | ## 💡 Почему это максимально просто 210 | 211 | ### Для пользователя: 212 | - 🎯 **Один клик для записи** (после первой авторизации) 213 | - 🎯 **Автоматическая отправка** - никаких дополнительных действий 214 | - 🎯 **Понятные уведомления** - всегда знает что происходит 215 | - 🎯 **Быстрый переход** - клик на уведомление → веб-интерфейс 216 | 217 | ### Для разработки: 218 | - 🎯 **Никаких новых API** - используем существующие endpoints 219 | - 🎯 **Стандартная авторизация** - OAuth через веб-интерфейс 220 | - 🎯 **Минимум кода** - простая логика состояний 221 | - 🎯 **Безопасность из коробки** - HTTP-only cookies 222 | 223 | ### Для безопасности: 224 | - 🎯 **Никакого storage** - токен только в сессии браузера 225 | - 🎯 **Стандартный OAuth** - проверенный механизм 226 | - 🎯 **Минимальные permissions** - только необходимые 227 | - 🎯 **Автоматическая очистка** - токен управляется браузером 228 | 229 | --- 230 | 231 | *Максимально простой флоу: открыл → авторизовался → записал → отправил → получил уведомление* 🎯 -------------------------------------------------------------------------------- /cursor-to-do/PROJECT_OVERVIEW.md: -------------------------------------------------------------------------------- 1 | # whisperx-fronted-docker-compose - Краткий обзор проекта 2 | 3 | > **Основано на [WhisperX](https://github.com/m-bain/whisperX)** by Max Bain 4 | > Лицензия BSD-2-Clause 5 | 6 | ## 📊 Статистика проекта 7 | - **Общий объем:** 16,437+ строк кода 8 | - **Backend:** 4,247 строк (Python) 9 | - **Frontend:** 9,247 строк (JavaScript/HTML/CSS) 10 | - **Chrome Extension:** 2,943 строки (JavaScript/HTML) 11 | - **Документация:** 15+ файлов 12 | 13 | ## 🗂️ Быстрая навигация по файлам 14 | 15 | ### 🖥️ Серверная часть (Backend) 16 | ``` 17 | src/ 18 | ├── main.py # 🚀 Точка входа FastAPI приложения 19 | ├── api/ 20 | │ ├── routes.py # 🛣️ Основные API маршруты (upload, status, etc.) 21 | │ ├── auth_routes.py # 🔐 Google OAuth аутентификация 22 | │ └── realtime_routes.py # 🔴 WebSocket API для real-time 23 | ├── core/ 24 | │ ├── whisper_manager.py # 🧠 Управление AI моделями 25 | │ └── transcription_processor.py # ⚙️ Обработка транскрипций 26 | ├── services/ 27 | │ ├── auth_service.py # 🔑 OAuth сервис 28 | │ ├── s3_service.py # ☁️ Yandex Cloud S3 29 | │ ├── database_service.py # 📊 JSON база данных 30 | │ └── subtitle_generator.py # 📄 Генерация форматов 31 | └── realtime/ 32 | ├── manager.py # 🎛️ Менеджер real-time сессий 33 | ├── processor.py # 🎤 Аудио обработка 34 | └── websocket_handler.py # 🔌 WebSocket обработчик 35 | ``` 36 | 37 | ### 🌐 Веб-интерфейс (Frontend) 38 | ``` 39 | web_interface/ 40 | ├── index.html # 🏠 Главная страница 41 | ├── login.html # 🔐 Страница входа 42 | ├── style.css # 🎨 Основные стили (1,988 строк) 43 | ├── config.js # ⚙️ Конфигурация клиента 44 | └── modules/ 45 | ├── main.js # 🎯 Главный контроллер приложения 46 | ├── api.js # 🌐 HTTP клиент 47 | ├── auth.js # 🔑 Аутентификация 48 | ├── transcription.js # 🎤 Управление транскрипцией 49 | ├── transcript.js # 📝 Отображение транскриптов 50 | ├── mediaPlayer.js # 🎵 Аудио/видео плеер 51 | ├── history.js # 📚 История транскрипций 52 | ├── downloads.js # 📥 Управление загрузками 53 | ├── summarization.js # 🤖 AI суммаризация 54 | ├── realtimeAudio.js # 🔴 Real-time аудио 55 | └── realtimeUI.js # 🎛️ Real-time интерфейс 56 | ``` 57 | 58 | ### 🔌 Chrome расширение 59 | ``` 60 | whisperx-fronted-docker-compose-extension/ 61 | ├── manifest.json # 📋 Манифест расширения 62 | ├── background.js # ⚙️ Service Worker 63 | ├── popup.html/js # 🎛️ Интерфейс расширения 64 | ├── offscreen.html/js # 🎤 Аудио микширование 65 | └── permission.html/js # 🔐 Управление разрешениями 66 | ``` 67 | 68 | ## 🔧 Ключевые функции по файлам 69 | 70 | ### 🎤 Транскрипция 71 | - **Загрузка файлов:** `web_interface/modules/fileHandler.js` 72 | - **Обработка:** `src/core/transcription_processor.py` 73 | - **AI модели:** `src/core/whisper_manager.py` 74 | - **Статус:** `src/api/routes.py` → `/api/status/{task_id}` 75 | 76 | ### 🔄 Real-Time 77 | - **WebSocket сервер:** `src/realtime/websocket_handler.py` 78 | - **Аудио захват:** `web_interface/modules/realtimeAudio.js` 79 | - **UI управление:** `web_interface/modules/realtimeUI.js` 80 | - **Обработка потока:** `src/realtime/processor.py` 81 | 82 | ### 🤖 AI Суммаризация 83 | - **Клиентская логика:** `web_interface/modules/summarization.js` 84 | - **API endpoint:** `src/api/routes.py` → `/api/summarize/{task_id}` 85 | - **Анализ спикеров:** встроено в `summarization.js` 86 | 87 | ### 🔐 Аутентификация 88 | - **Google OAuth:** `src/services/auth_service.py` 89 | - **JWT токены:** `src/api/auth_routes.py` 90 | - **Клиентская часть:** `web_interface/modules/auth.js` 91 | - **Middleware:** `src/middleware/auth_middleware.py` 92 | 93 | ### ☁️ Облачное хранение 94 | - **S3 сервис:** `src/services/s3_service.py` 95 | - **Загрузка файлов:** автоматическая после транскрипции 96 | - **Ссылки:** `src/api/routes.py` → `/api/s3-links/{task_id}` 97 | 98 | ### 📊 База данных 99 | - **JSON DB:** `src/services/database_service.py` 100 | - **Файл данных:** `data/transcriptions_db.json` 101 | - **История:** `web_interface/modules/history.js` 102 | 103 | ## 🚀 Точки входа 104 | 105 | ### Запуск приложения 106 | ```bash 107 | python run.py # 🚀 Backend + Frontend 108 | python server.py # 🖥️ Только API сервер 109 | python dev.py # 🔧 Режим разработки 110 | ``` 111 | 112 | ### Docker 113 | ```bash 114 | docker compose up -d # 🐳 Базовый запуск 115 | docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d # 🚀 С GPU 116 | ``` 117 | 118 | ### Доступ 119 | - **Web UI:** http://localhost:8000 120 | - **API:** http://localhost:8880 121 | - **Docs:** http://localhost:8880/docs 122 | - **WebSocket:** ws://localhost:8880/ws/realtime 123 | 124 | ## 🔍 Поиск функций 125 | 126 | ### Хочу найти код для... 127 | 128 | | Функция | Файл | Строка/Метод | 129 | |---------|------|--------------| 130 | | **Загрузка файла** | `web_interface/modules/fileHandler.js` | `handleFileSelect()` | 131 | | **Начало транскрипции** | `src/api/routes.py` | `upload_file()` | 132 | | **Статус обработки** | `src/api/routes.py` | `get_status()` | 133 | | **Отображение транскрипта** | `web_interface/modules/transcript.js` | `displayTranscript()` | 134 | | **Real-time подключение** | `web_interface/modules/realtimeAudio.js` | `connect()` | 135 | | **Создание саммари** | `web_interface/modules/summarization.js` | `createSummary()` | 136 | | **OAuth авторизация** | `src/services/auth_service.py` | `get_oauth_url()` | 137 | | **Загрузка на S3** | `src/services/s3_service.py` | `upload_file()` | 138 | | **Генерация субтитров** | `src/services/subtitle_generator.py` | `generate_*()` | 139 | | **История транскрипций** | `web_interface/modules/history.js` | `loadTranscriptionHistory()` | 140 | 141 | ## 🎨 Стили и UI 142 | 143 | ### CSS файлы 144 | - **Основные стили:** `web_interface/style.css` (1,988 строк) 145 | - **Real-time UI:** `web_interface/css/realtime.css` (605 строк) 146 | - **Chrome extension:** встроено в `whisperx-fronted-docker-compose-extension/popup.html` 147 | 148 | ### Ключевые UI компоненты 149 | - **Drag & Drop:** `.upload-area` в `style.css` 150 | - **Медиаплеер:** `.media-player` в `style.css` 151 | - **Транскрипт:** `.transcript-container` в `style.css` 152 | - **История:** `.history-section` в `style.css` 153 | - **Real-time панель:** `.realtime-panel` в `realtime.css` 154 | 155 | ## 📋 Конфигурация 156 | 157 | ### Основные конфиги 158 | - **Сервер:** `src/config/settings.py` 159 | - **Клиент:** `web_interface/config.js` 160 | - **Docker:** `docker-compose.*.yml` 161 | - **Chrome extension:** `whisperx-fronted-docker-compose-extension/manifest.json` 162 | 163 | ### Переменные окружения 164 | ```bash 165 | S3_ACCESS_KEY # Yandex Cloud S3 166 | S3_SECRET_KEY # Yandex Cloud S3 167 | S3_BUCKET # Имя bucket 168 | GOOGLE_CLIENT_ID # OAuth клиент 169 | GOOGLE_CLIENT_SECRET # OAuth секрет 170 | JWT_SECRET_KEY # JWT подпись 171 | ``` 172 | 173 | ## 🐛 Отладка 174 | 175 | ### Логи и ошибки 176 | - **FastAPI логи:** автоматически в консоль 177 | - **JavaScript ошибки:** браузерная консоль 178 | - **Chrome extension:** `chrome://extensions/` → Developer mode 179 | 180 | ### Полезные endpoints для отладки 181 | - `GET /api/status/{task_id}` - статус транскрипции 182 | - `GET /api/transcriptions` - список всех транскрипций 183 | - `GET /docs` - автоматическая документация API 184 | - `WebSocket /ws/realtime` - тестирование real-time 185 | 186 | --- 187 | 188 | *Этот файл создан для быстрой навигации по проекту whisperx-fronted-docker-compose. Для полной документации см. README.md* -------------------------------------------------------------------------------- /src/realtime/manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Менеджер real-time транскрипции 3 | 4 | Управляет сессиями real-time транскрипции и интегрируется с существующим WhisperX. 5 | """ 6 | 7 | import asyncio 8 | import logging 9 | from typing import Dict, Optional, List 10 | from datetime import datetime 11 | 12 | from .models import ( 13 | SessionConfig, SessionStatus, create_session_id, 14 | create_transcription_partial, create_transcription_final, 15 | create_error_event 16 | ) 17 | from ..core.whisper_manager import WhisperManager 18 | from .processor import StreamingAudioProcessor 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class RealtimeTranscriptionManager: 24 | """ 25 | Менеджер real-time транскрипции 26 | 27 | Управляет активными сессиями, интегрируется с существующим WhisperManager 28 | и координирует потоковую обработку аудио. 29 | """ 30 | 31 | def __init__(self, whisper_manager: Optional[WhisperManager] = None): 32 | """ 33 | Инициализация менеджера 34 | 35 | Args: 36 | whisper_manager: Существующий WhisperManager или None для создания нового 37 | """ 38 | self.whisper_manager = whisper_manager or WhisperManager() 39 | self.active_sessions: Dict[str, SessionStatus] = {} 40 | self.processors: Dict[str, StreamingAudioProcessor] = {} 41 | self.max_sessions = 10 # Максимальное количество одновременных сессий 42 | 43 | logger.info("RealtimeTranscriptionManager initialized") 44 | 45 | async def start_session(self, config: SessionConfig) -> str: 46 | """ 47 | Начать новую сессию real-time транскрипции 48 | 49 | Args: 50 | config: Конфигурация сессии 51 | 52 | Returns: 53 | str: ID созданной сессии 54 | 55 | Raises: 56 | RuntimeError: Если превышено максимальное количество сессий 57 | """ 58 | if len(self.active_sessions) >= self.max_sessions: 59 | raise RuntimeError(f"Maximum sessions limit reached: {self.max_sessions}") 60 | 61 | session_id = create_session_id() 62 | 63 | # Создать статус сессии 64 | session_status = SessionStatus( 65 | session_id=session_id, 66 | is_active=True, 67 | start_time=datetime.utcnow(), 68 | last_activity=datetime.utcnow(), 69 | config=config, 70 | stats={ 71 | "chunks_processed": 0, 72 | "total_duration_ms": 0, 73 | "avg_confidence": 0.0, 74 | "errors_count": 0 75 | } 76 | ) 77 | 78 | # Создать процессор для сессии 79 | processor = StreamingAudioProcessor( 80 | config=config, 81 | whisper_manager=self.whisper_manager 82 | ) 83 | 84 | # Сохранить сессию 85 | self.active_sessions[session_id] = session_status 86 | self.processors[session_id] = processor 87 | 88 | logger.info(f"Started realtime session: {session_id}") 89 | return session_id 90 | 91 | async def stop_session(self, session_id: str) -> bool: 92 | """ 93 | Остановить сессию real-time транскрипции 94 | 95 | Args: 96 | session_id: ID сессии 97 | 98 | Returns: 99 | bool: True если сессия была остановлена, False если не найдена 100 | """ 101 | if session_id not in self.active_sessions: 102 | logger.warning(f"Session not found: {session_id}") 103 | return False 104 | 105 | # Остановить процессор 106 | if session_id in self.processors: 107 | await self.processors[session_id].cleanup() 108 | del self.processors[session_id] 109 | 110 | # Обновить статус 111 | self.active_sessions[session_id].is_active = False 112 | 113 | # Удалить из активных сессий 114 | del self.active_sessions[session_id] 115 | 116 | logger.info(f"Stopped realtime session: {session_id}") 117 | return True 118 | 119 | async def process_audio_chunk(self, session_id: str, audio_data: bytes, sequence: int) -> Optional[str]: 120 | """ 121 | Обработать аудио чанк для сессии 122 | 123 | Args: 124 | session_id: ID сессии 125 | audio_data: Аудио данные 126 | sequence: Порядковый номер чанка 127 | 128 | Returns: 129 | Optional[str]: Результат транскрипции или None 130 | """ 131 | if session_id not in self.active_sessions: 132 | logger.error(f"Session not found: {session_id}") 133 | return None 134 | 135 | if session_id not in self.processors: 136 | logger.error(f"Processor not found for session: {session_id}") 137 | return None 138 | 139 | try: 140 | # Обновить время последней активности 141 | self.active_sessions[session_id].last_activity = datetime.utcnow() 142 | 143 | # Обработать чанк 144 | processor = self.processors[session_id] 145 | result = await processor.process_chunk(audio_data, sequence) 146 | 147 | # Обновить статистику 148 | stats = self.active_sessions[session_id].stats 149 | stats["chunks_processed"] += 1 150 | stats["total_duration_ms"] += 100 # Предполагаем 100ms чанки 151 | 152 | return result 153 | 154 | except Exception as e: 155 | logger.error(f"Error processing audio chunk for session {session_id}: {e}") 156 | 157 | # Обновить статистику ошибок 158 | self.active_sessions[session_id].stats["errors_count"] += 1 159 | 160 | return None 161 | 162 | async def get_partial_result(self, session_id: str) -> Optional[str]: 163 | """ 164 | Получить частичный результат транскрипции 165 | 166 | Args: 167 | session_id: ID сессии 168 | 169 | Returns: 170 | Optional[str]: Частичный результат или None 171 | """ 172 | if session_id not in self.processors: 173 | return None 174 | 175 | try: 176 | processor = self.processors[session_id] 177 | return await processor.get_partial_result() 178 | except Exception as e: 179 | logger.error(f"Error getting partial result for session {session_id}: {e}") 180 | return None 181 | 182 | def get_session_status(self, session_id: str) -> Optional[SessionStatus]: 183 | """ 184 | Получить статус сессии 185 | 186 | Args: 187 | session_id: ID сессии 188 | 189 | Returns: 190 | Optional[SessionStatus]: Статус сессии или None 191 | """ 192 | return self.active_sessions.get(session_id) 193 | 194 | def get_active_sessions(self) -> List[str]: 195 | """ 196 | Получить список активных сессий 197 | 198 | Returns: 199 | List[str]: Список ID активных сессий 200 | """ 201 | return list(self.active_sessions.keys()) 202 | 203 | async def cleanup_inactive_sessions(self, timeout_minutes: int = 30): 204 | """ 205 | Очистить неактивные сессии 206 | 207 | Args: 208 | timeout_minutes: Таймаут неактивности в минутах 209 | """ 210 | current_time = datetime.utcnow() 211 | inactive_sessions = [] 212 | 213 | for session_id, status in self.active_sessions.items(): 214 | inactive_duration = (current_time - status.last_activity).total_seconds() / 60 215 | if inactive_duration > timeout_minutes: 216 | inactive_sessions.append(session_id) 217 | 218 | for session_id in inactive_sessions: 219 | logger.info(f"Cleaning up inactive session: {session_id}") 220 | await self.stop_session(session_id) 221 | 222 | async def get_system_stats(self) -> Dict[str, any]: 223 | """ 224 | Получить системную статистику 225 | 226 | Returns: 227 | Dict: Статистика системы 228 | """ 229 | total_chunks = sum(s.stats.get("chunks_processed", 0) for s in self.active_sessions.values()) 230 | total_errors = sum(s.stats.get("errors_count", 0) for s in self.active_sessions.values()) 231 | 232 | return { 233 | "active_sessions": len(self.active_sessions), 234 | "max_sessions": self.max_sessions, 235 | "total_chunks_processed": total_chunks, 236 | "total_errors": total_errors, 237 | "whisper_model_loaded": self.whisper_manager.model is not None if self.whisper_manager else False 238 | } -------------------------------------------------------------------------------- /src/core/whisper_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Менеджер для работы с моделями WhisperX 3 | """ 4 | import os 5 | import threading 6 | import torch 7 | from typing import Optional, Callable 8 | 9 | import whisperx 10 | 11 | from ..models.schemas import TranscriptionConfig 12 | 13 | 14 | class WhisperManager: 15 | """Менеджер для работы с моделями WhisperX""" 16 | 17 | def __init__(self): 18 | self.model = None 19 | self.align_model = None 20 | self.align_metadata = None 21 | self.diarize_model = None 22 | self.models_loaded = False 23 | self.loading_lock = threading.Lock() 24 | self.device = self._detect_device() 25 | self.compute_type = self._detect_compute_type() 26 | print(f"🔧 Обнаружено устройство: {self.device}, compute_type: {self.compute_type}") 27 | 28 | def _detect_device(self) -> str: 29 | """Определение доступного устройства""" 30 | if torch.cuda.is_available(): 31 | return "cuda" 32 | else: 33 | return "cpu" 34 | 35 | def _detect_compute_type(self) -> str: 36 | """Автоматическое определение compute_type""" 37 | if self.device == "cuda": 38 | # Проверяем поддержку float16 на GPU 39 | try: 40 | # Пробуем создать тензор float16 на GPU 41 | test_tensor = torch.tensor([1.0], dtype=torch.float16, device="cuda") 42 | return "float16" 43 | except Exception: 44 | return "float32" 45 | else: 46 | # Для CPU используем int8 для лучшей производительности 47 | return "int8" 48 | 49 | def load_models(self, config: TranscriptionConfig, status_callback: Optional[Callable] = None): 50 | """Загрузка моделей WhisperX в память""" 51 | with self.loading_lock: 52 | if self.models_loaded: 53 | return 54 | 55 | # Определяем compute_type 56 | compute_type = config.compute_type 57 | if compute_type == "auto": 58 | compute_type = self.compute_type 59 | print(f"🔧 Автоматически выбран compute_type: {compute_type}") 60 | 61 | if status_callback: 62 | status_callback("loading_whisper_model", "Загрузка модели Whisper...", 20) 63 | print(f"🔧 Загрузка модели Whisper: {config.model}") 64 | self.model = whisperx.load_model( 65 | config.model, 66 | self.device, 67 | compute_type=compute_type 68 | ) 69 | 70 | if status_callback: 71 | status_callback("loading_align_model", "Загрузка модели выравнивания...", 25) 72 | print("🔧 Загрузка модели выравнивания...") 73 | 74 | # Попытка загрузить модель выравнивания с обработкой ошибок 75 | try: 76 | self.align_model, self.align_metadata = whisperx.load_align_model( 77 | language_code=config.language, 78 | device=self.device 79 | ) 80 | except Exception as e: 81 | print(f"⚠️ Не удалось загрузить модель выравнивания для языка '{config.language}': {e}") 82 | print("🔧 Попытка загрузить универсальную модель выравнивания...") 83 | try: 84 | # Попробуем загрузить для английского языка как fallback 85 | self.align_model, self.align_metadata = whisperx.load_align_model( 86 | language_code="en", 87 | device=self.device 88 | ) 89 | print("✅ Загружена английская модель выравнивания как fallback") 90 | except Exception as e2: 91 | print(f"❌ Не удалось загрузить модель выравнивания: {e2}") 92 | print("⚠️ Транскрипция будет выполнена без точного выравнивания временных меток") 93 | self.align_model = None 94 | self.align_metadata = None 95 | 96 | if config.diarize and config.hf_token: 97 | if status_callback: 98 | status_callback("loading_diarize_model", "Загрузка модели диаризации...", 28) 99 | print("🔧 Загрузка модели диаризации...") 100 | print(f"🔑 HF Token для диаризации: {config.hf_token[:20]}...{config.hf_token[-10:] if len(config.hf_token) > 30 else config.hf_token}") 101 | print(f"🔑 Длина токена: {len(config.hf_token)} символов") 102 | print(f"🔑 Токен начинается с 'hf_': {config.hf_token.startswith('hf_')}") 103 | self.diarize_model = whisperx.diarize.DiarizationPipeline( 104 | use_auth_token=config.hf_token, 105 | device=self.device 106 | ) 107 | 108 | self.models_loaded = True 109 | print("✅ Модели загружены успешно!") 110 | 111 | def transcribe_audio(self, audio_path: str, config: TranscriptionConfig, status_callback: Optional[Callable] = None) -> dict: 112 | """ 113 | Выполнение транскрипции аудио 114 | 115 | Args: 116 | audio_path: Путь к аудио файлу 117 | config: Конфигурация транскрипции 118 | status_callback: Callback для обновления статуса 119 | 120 | Returns: 121 | Результат транскрипции 122 | """ 123 | if not self.models_loaded: 124 | self.load_models(config, status_callback) 125 | 126 | # Загружаем аудио 127 | if status_callback: 128 | status_callback("loading_audio", "Загрузка аудио файла...", 32) 129 | print(f"🎵 Загрузка аудио файла: {audio_path}") 130 | audio = whisperx.load_audio(audio_path) 131 | 132 | # Транскрипция 133 | if status_callback: 134 | status_callback("transcribing", "Выполнение транскрипции...", 45) 135 | print("🎯 Выполнение транскрипции...") 136 | result = self.model.transcribe(audio, batch_size=config.batch_size) 137 | 138 | # Выравнивание 139 | if self.align_model and self.align_metadata: 140 | if status_callback: 141 | status_callback("aligning", "Выравнивание текста...", 65) 142 | print("📐 Выравнивание текста...") 143 | result = whisperx.align( 144 | result["segments"], 145 | self.align_model, 146 | self.align_metadata, 147 | audio, 148 | self.device 149 | ) 150 | 151 | # Диаризация (если включена) 152 | if config.diarize and self.diarize_model: 153 | if status_callback: 154 | status_callback("diarizing", "Диаризация спикеров...", 72) 155 | print("👥 Диаризация спикеров...") 156 | diarize_segments = self.diarize_model(audio) 157 | result = whisperx.assign_word_speakers(diarize_segments, result) 158 | 159 | return result 160 | 161 | async def transcribe_audio_chunk(self, audio_data, sample_rate: int = 16000, language: str = "ru") -> str: 162 | """ 163 | Транскрипция аудио чанка для real-time режима 164 | 165 | Args: 166 | audio_data: Numpy array с аудио данными 167 | sample_rate: Частота дискретизации 168 | language: Язык для транскрипции 169 | 170 | Returns: 171 | str: Результат транскрипции 172 | """ 173 | if not self.models_loaded: 174 | # Для real-time нужно загрузить базовую конфигурацию 175 | from ..models.schemas import TranscriptionConfig 176 | basic_config = TranscriptionConfig( 177 | model="base", 178 | language=language, 179 | compute_type="auto", 180 | batch_size=16, 181 | diarize=False, 182 | hf_token="" 183 | ) 184 | self.load_models(basic_config) 185 | 186 | try: 187 | # Убеждаемся, что audio_data - это numpy array float32 188 | import numpy as np 189 | if not isinstance(audio_data, np.ndarray): 190 | audio_data = np.array(audio_data, dtype=np.float32) 191 | elif audio_data.dtype != np.float32: 192 | audio_data = audio_data.astype(np.float32) 193 | 194 | # WhisperX ожидает аудио с частотой 16kHz, нужно ресемплировать если нужно 195 | if sample_rate != 16000: 196 | # Простое ресемплирование (в продакшене лучше использовать librosa) 197 | import scipy.signal 198 | target_length = int(len(audio_data) * 16000 / sample_rate) 199 | audio_data = scipy.signal.resample(audio_data, target_length) 200 | 201 | # Транскрибируем аудио чанк 202 | result = self.model.transcribe(audio_data, batch_size=1) 203 | 204 | # Извлекаем текст из результата 205 | if result and "segments" in result and result["segments"]: 206 | text_parts = [] 207 | for segment in result["segments"]: 208 | if "text" in segment: 209 | text_parts.append(segment["text"].strip()) 210 | 211 | return " ".join(text_parts).strip() 212 | 213 | return "" 214 | 215 | except Exception as e: 216 | print(f"❌ Ошибка транскрипции чанка: {e}") 217 | return "" 218 | 219 | @property 220 | def is_loaded(self) -> bool: 221 | """Проверка загружены ли модели""" 222 | return self.models_loaded -------------------------------------------------------------------------------- /src/services/database_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Сервис для работы с JSON базой данных 3 | """ 4 | import json 5 | import threading 6 | from typing import Dict, List, Optional 7 | from datetime import datetime 8 | 9 | from ..config.settings import DATABASE_FILE 10 | 11 | 12 | class DatabaseService: 13 | """Сервис для работы с JSON базой данных""" 14 | 15 | def __init__(self): 16 | self.lock = threading.Lock() 17 | 18 | def load_database(self) -> Dict: 19 | """Загрузка базы данных из JSON файла""" 20 | with self.lock: 21 | if DATABASE_FILE.exists(): 22 | try: 23 | with open(DATABASE_FILE, 'r', encoding='utf-8') as f: 24 | data = json.load(f) 25 | # Обеспечиваем структуру базы данных 26 | if 'transcriptions' not in data: 27 | data['transcriptions'] = {} 28 | if 'users' not in data: 29 | data['users'] = {} 30 | if 'sessions' not in data: 31 | data['sessions'] = {} 32 | return data 33 | except (json.JSONDecodeError, Exception) as e: 34 | print(f"⚠️ Ошибка загрузки базы данных: {e}") 35 | return {'transcriptions': {}, 'users': {}, 'sessions': {}} 36 | return {'transcriptions': {}, 'users': {}, 'sessions': {}} 37 | 38 | def save_database(self, db_data: Dict): 39 | """Сохранение базы данных в JSON файл""" 40 | with self.lock: 41 | try: 42 | # Конвертируем datetime объекты в строки для JSON сериализации 43 | def convert_datetime(obj): 44 | if isinstance(obj, datetime): 45 | return obj.isoformat() 46 | elif isinstance(obj, dict): 47 | return {k: convert_datetime(v) for k, v in obj.items()} 48 | elif isinstance(obj, list): 49 | return [convert_datetime(item) for item in obj] 50 | return obj 51 | 52 | serializable_data = convert_datetime(db_data) 53 | 54 | with open(DATABASE_FILE, 'w', encoding='utf-8') as f: 55 | json.dump(serializable_data, f, ensure_ascii=False, indent=2) 56 | except Exception as e: 57 | print(f"❌ Ошибка сохранения базы данных: {e}") 58 | 59 | # Методы для работы с транскрипциями 60 | def add_transcription(self, transcription_data: Dict): 61 | """Добавление транскрипции в базу данных""" 62 | db = self.load_database() 63 | db['transcriptions'][transcription_data['id']] = transcription_data 64 | self.save_database(db) 65 | print(f"✅ Транскрипция {transcription_data['id']} добавлена в базу данных") 66 | 67 | def get_transcription(self, task_id: str) -> Optional[Dict]: 68 | """Получение транскрипции из базы данных""" 69 | db = self.load_database() 70 | return db['transcriptions'].get(task_id) 71 | 72 | def update_transcription(self, task_id: str, updates: Dict): 73 | """Обновление транскрипции в базе данных""" 74 | db = self.load_database() 75 | if task_id in db['transcriptions']: 76 | db['transcriptions'][task_id].update(updates) 77 | self.save_database(db) 78 | print(f"✅ Транскрипция {task_id} обновлена в базе данных") 79 | 80 | def delete_transcription(self, task_id: str) -> bool: 81 | """Удаление транскрипции из базы данных""" 82 | db = self.load_database() 83 | if task_id in db['transcriptions']: 84 | del db['transcriptions'][task_id] 85 | self.save_database(db) 86 | print(f"✅ Транскрипция {task_id} удалена из базы данных") 87 | return True 88 | return False 89 | 90 | def get_all_transcriptions(self) -> List[Dict]: 91 | """Получение всех транскрипций из базы данных""" 92 | db = self.load_database() 93 | # Сортируем по дате создания (новые сначала) 94 | transcriptions = list(db['transcriptions'].values()) 95 | transcriptions.sort(key=lambda x: x.get('created_at', ''), reverse=True) 96 | return transcriptions 97 | 98 | def get_user_transcriptions(self, user_id: str) -> List[Dict]: 99 | """Получение транскрипций пользователя""" 100 | db = self.load_database() 101 | user_transcriptions = [] 102 | 103 | for transcription in db['transcriptions'].values(): 104 | if transcription.get('user_id') == user_id: 105 | user_transcriptions.append(transcription) 106 | 107 | # Сортируем по дате создания (новые сначала) 108 | user_transcriptions.sort(key=lambda x: x.get('created_at', ''), reverse=True) 109 | return user_transcriptions 110 | 111 | def create_transcription_record(self, task_id: str, filename: str, status: str = "pending", **kwargs) -> Dict: 112 | """Создание записи транскрипции""" 113 | record = { 114 | "id": task_id, 115 | "filename": filename, 116 | "status": status, 117 | "created_at": datetime.now().isoformat(), 118 | "s3_links": {}, 119 | **kwargs 120 | } 121 | return record 122 | 123 | def create_error_record(self, task_id: str, filename: str, error_msg: str, user_id: str = None) -> Dict: 124 | """Создание записи об ошибке""" 125 | return self.create_transcription_record( 126 | task_id=task_id, 127 | filename=filename, 128 | status="failed", 129 | error=error_msg, 130 | user_id=user_id 131 | ) 132 | 133 | def create_completed_record(self, task_id: str, filename: str, s3_links: Dict, **kwargs) -> Dict: 134 | """Создание записи о завершенной транскрипции""" 135 | return self.create_transcription_record( 136 | task_id=task_id, 137 | filename=filename, 138 | status="completed", 139 | completed_at=datetime.now().isoformat(), 140 | s3_links=s3_links, 141 | **kwargs 142 | ) 143 | 144 | # Методы для работы с пользователями 145 | def create_user(self, user_data: Dict): 146 | """Создание пользователя в базе данных""" 147 | db = self.load_database() 148 | db['users'][user_data['id']] = user_data 149 | self.save_database(db) 150 | print(f"✅ Пользователь {user_data['email']} создан в базе данных") 151 | 152 | def get_user(self, user_id: str) -> Optional[Dict]: 153 | """Получение пользователя по ID""" 154 | db = self.load_database() 155 | return db['users'].get(user_id) 156 | 157 | def get_user_by_email(self, email: str) -> Optional[Dict]: 158 | """Получение пользователя по email""" 159 | db = self.load_database() 160 | for user in db['users'].values(): 161 | if user.get('email') == email: 162 | return user 163 | return None 164 | 165 | def get_user_by_google_id(self, google_id: str) -> Optional[Dict]: 166 | """Получение пользователя по Google ID""" 167 | db = self.load_database() 168 | for user in db['users'].values(): 169 | if user.get('google_id') == google_id: 170 | return user 171 | return None 172 | 173 | def update_user(self, user_id: str, updates: Dict): 174 | """Обновление пользователя в базе данных""" 175 | db = self.load_database() 176 | if user_id in db['users']: 177 | db['users'][user_id].update(updates) 178 | self.save_database(db) 179 | print(f"✅ Пользователь {user_id} обновлен в базе данных") 180 | 181 | def get_users(self) -> List[Dict]: 182 | """Получение всех пользователей""" 183 | db = self.load_database() 184 | return list(db['users'].values()) 185 | 186 | # Методы для работы с сессиями 187 | def create_user_session(self, session_data: Dict): 188 | """Создание пользовательской сессии""" 189 | db = self.load_database() 190 | db['sessions'][session_data['session_token']] = session_data 191 | self.save_database(db) 192 | print(f"✅ Сессия для пользователя {session_data['user_id']} создана") 193 | 194 | def get_user_session(self, session_token: str) -> Optional[Dict]: 195 | """Получение сессии по токену""" 196 | db = self.load_database() 197 | return db['sessions'].get(session_token) 198 | 199 | def delete_user_session(self, session_token: str) -> bool: 200 | """Удаление пользовательской сессии""" 201 | db = self.load_database() 202 | if session_token in db['sessions']: 203 | del db['sessions'][session_token] 204 | self.save_database(db) 205 | print(f"✅ Сессия {session_token} удалена") 206 | return True 207 | return False 208 | 209 | def delete_user_sessions(self, user_id: str) -> int: 210 | """Удаление всех сессий пользователя""" 211 | db = self.load_database() 212 | deleted_count = 0 213 | 214 | sessions_to_delete = [] 215 | for session_token, session_data in db['sessions'].items(): 216 | if session_data.get('user_id') == user_id: 217 | sessions_to_delete.append(session_token) 218 | 219 | for session_token in sessions_to_delete: 220 | del db['sessions'][session_token] 221 | deleted_count += 1 222 | 223 | if deleted_count > 0: 224 | self.save_database(db) 225 | print(f"✅ Удалено {deleted_count} сессий пользователя {user_id}") 226 | 227 | return deleted_count -------------------------------------------------------------------------------- /cursor-to-do/EXTENSION_TODO_PLAN.md: -------------------------------------------------------------------------------- 1 | # 📋 TODO План - whisperx-fronted-docker-compose Chrome Extension (С простой авторизацией) 2 | 3 | ## 🎯 Цель проекта 4 | Chrome расширение с простой авторизацией: проверить статус → записать → загрузить → уведомление 5 | 6 | **Важно**: Записи должны быть привязаны к пользователю! 7 | 8 | **Реальный API**: `https://redmadtranscribe-api.neuraldeep.tech/` 9 | 10 | --- 11 | 12 | ## 📅 День 1: Базовая структура и авторизация 13 | 14 | ### ✅ 1.1 Создание структуры проекта 15 | - [ ] Создать папку `whisperx-fronted-docker-compose-extension/` 16 | - [ ] Создать базовую структуру файлов: 17 | ``` 18 | whisperx-fronted-docker-compose-extension/ 19 | ├── manifest.json 20 | ├── popup/ 21 | │ ├── popup.html 22 | │ ├── popup.js 23 | │ └── popup.css 24 | ├── background.js 25 | └── icons/ 26 | ├── icon16.png 27 | ├── icon48.png 28 | └── icon128.png 29 | ``` 30 | 31 | ### ✅ 1.2 Настройка manifest.json без storage 32 | - [ ] Создать `manifest.json` для Manifest V3 с минимальными permissions: 33 | ```json 34 | { 35 | "manifest_version": 3, 36 | "name": "whisperx-fronted-docker-compose Meeting Recorder", 37 | "version": "1.0.0", 38 | "description": "Record meeting audio and transcribe with whisperx-fronted-docker-compose", 39 | "permissions": [ 40 | "tabCapture", 41 | "activeTab", 42 | "notifications" 43 | ] 44 | } 45 | ``` 46 | 47 | ### ✅ 1.3 Создание UI с авторизацией 48 | - [ ] Создать `popup/popup.html`: 49 | - Секция авторизации (login/user info) 50 | - Кнопка "Войти через Google" 51 | - Информация о пользователе + кнопка "Выйти" 52 | - Секция записи (показывается только после авторизации) 53 | - Кнопки записи и таймер 54 | 55 | - [ ] Создать `popup/popup.css`: 56 | - Стили для секций авторизации и записи 57 | - Скрытие/показ элементов в зависимости от статуса 58 | - Пульсирующая анимация для записи 59 | 60 | ### ✅ 1.4 Реализация проверки авторизации 61 | - [ ] В `popup.js` создать функции: 62 | ```javascript 63 | // Проверка статуса авторизации 64 | async function checkAuthStatus() { 65 | const response = await fetch(`${API_BASE}/auth/status`, { 66 | credentials: 'include' 67 | }); 68 | const data = await response.json(); 69 | 70 | if (data.authenticated && data.user) { 71 | showAuthenticatedState(data.user); 72 | } else { 73 | showUnauthenticatedState(); 74 | } 75 | } 76 | 77 | // Показ/скрытие UI элементов 78 | function showAuthenticatedState(user) { 79 | // Показать секцию записи, скрыть логин 80 | } 81 | 82 | function showUnauthenticatedState() { 83 | // Показать логин, скрыть секцию записи 84 | } 85 | ``` 86 | 87 | ### ✅ 1.5 Тестирование авторизации 88 | - [ ] Загрузить расширение в Chrome 89 | - [ ] Протестировать проверку статуса авторизации 90 | - [ ] Проверить переход на страницу входа 91 | - [ ] Убедиться что UI корректно переключается 92 | 93 | --- 94 | 95 | ## 📅 День 2: Запись аудио и интеграция с API 96 | 97 | ### ✅ 2.1 Реализация записи аудио 98 | - [ ] В `background.js` создать функции записи: 99 | ```javascript 100 | let currentUser = null; 101 | 102 | async function startRecording() { 103 | if (!currentUser) { 104 | throw new Error('Нет данных пользователя'); 105 | } 106 | 107 | const stream = await chrome.tabCapture.capture({ 108 | audio: true, 109 | video: false 110 | }); 111 | 112 | const recorder = new MediaRecorder(stream, { 113 | mimeType: 'audio/webm;codecs=opus', 114 | audioBitsPerSecond: 128000 115 | }); 116 | 117 | // Логика записи... 118 | } 119 | ``` 120 | 121 | ### ✅ 2.2 Передача данных пользователя 122 | - [ ] Передача пользователя из popup в background: 123 | ```javascript 124 | // В popup.js 125 | const response = await chrome.runtime.sendMessage({ 126 | action: 'startRecording', 127 | user: currentUser // Передаем данные пользователя 128 | }); 129 | 130 | // В background.js 131 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 132 | if (request.action === 'startRecording') { 133 | currentUser = request.user; // Сохраняем в памяти 134 | startRecording().then(sendResponse); 135 | } 136 | }); 137 | ``` 138 | 139 | ### ✅ 2.3 Интеграция с API загрузки 140 | - [ ] Создать функцию загрузки с привязкой к пользователю: 141 | ```javascript 142 | async function uploadRecording() { 143 | if (!currentUser) { 144 | throw new Error('Нет данных пользователя'); 145 | } 146 | 147 | const audioBlob = new Blob(recordedChunks, {type: 'audio/webm'}); 148 | const formData = new FormData(); 149 | 150 | // Включаем email пользователя в имя файла 151 | formData.append('file', audioBlob, `meeting-${currentUser.email}-${Date.now()}.webm`); 152 | formData.append('diarize', 'true'); 153 | formData.append('language', 'auto'); 154 | 155 | const response = await fetch('https://redmadtranscribe-api.neuraldeep.tech/api/upload', { 156 | method: 'POST', 157 | body: formData, 158 | credentials: 'include' 159 | }); 160 | } 161 | ``` 162 | 163 | ### ✅ 2.4 Обработка ошибок авторизации 164 | - [ ] Обработка ошибки 401 (неавторизован): 165 | ```javascript 166 | if (response.status === 401) { 167 | notifyPopup('uploadError', 'Ошибка авторизации. Войдите заново.'); 168 | } 169 | ``` 170 | 171 | - [ ] Очистка данных пользователя после загрузки: 172 | ```javascript 173 | } finally { 174 | currentUser = null; // Очищаем из памяти 175 | } 176 | ``` 177 | 178 | ### ✅ 2.5 Система уведомлений 179 | - [ ] Уведомление с именем пользователя: 180 | ```javascript 181 | chrome.notifications.create('whisperx2-upload', { 182 | title: 'whisperx-fronted-docker-compose - Запись загружена!', 183 | message: `Транскрипция для ${currentUser.name || currentUser.email} в процессе.`, 184 | buttons: [{title: 'Открыть whisperx-fronted-docker-compose'}] 185 | }); 186 | ``` 187 | 188 | ### ✅ 2.6 Тестирование записи 189 | - [ ] Протестировать запись с авторизованным пользователем 190 | - [ ] Проверить что файл содержит email пользователя 191 | - [ ] Убедиться что запись привязана к правильному аккаунту 192 | - [ ] Тестировать обработку ошибок авторизации 193 | 194 | --- 195 | 196 | ## 📅 День 3: Полировка и финализация 197 | 198 | ### ✅ 3.1 Улучшение UX авторизации 199 | - [ ] Добавить индикаторы состояния: 200 | - "Проверяем авторизацию..." 201 | - "Готов к записи встреч" 202 | - "Необходима авторизация" 203 | 204 | - [ ] Улучшить переходы между состояниями: 205 | - Плавное показ/скрытие секций 206 | - Корректное отображение информации о пользователе 207 | 208 | ### ✅ 3.2 Обработка edge cases 209 | - [ ] Пользователь не авторизован при попытке записи: 210 | ```javascript 211 | if (!currentUser) { 212 | status.textContent = 'Ошибка: нет авторизации'; 213 | return; 214 | } 215 | ``` 216 | 217 | - [ ] Сессия истекла во время записи: 218 | - Показать ошибку авторизации 219 | - Предложить войти заново 220 | 221 | - [ ] Пользователь вышел в другой вкладке: 222 | - Периодическая проверка статуса (опционально) 223 | 224 | ### ✅ 3.3 Определение типа встречи 225 | - [ ] Добавить платформу встречи в метаданные: 226 | ```javascript 227 | function detectMeetingPlatform() { 228 | const url = window.location.href; 229 | if (url.includes('zoom.us')) return 'Zoom'; 230 | if (url.includes('meet.google.com')) return 'Google Meet'; 231 | if (url.includes('teams.microsoft.com')) return 'Microsoft Teams'; 232 | return 'Meeting'; 233 | } 234 | 235 | // В имени файла 236 | const platform = detectMeetingPlatform(); 237 | const filename = `${platform}-${currentUser.email}-${Date.now()}.webm`; 238 | ``` 239 | 240 | ### ✅ 3.4 Финальное тестирование 241 | - [ ] Полный цикл: авторизация → запись → загрузка → уведомление 242 | - [ ] Тестирование на разных платформах встреч 243 | - [ ] Проверка обработки всех ошибок 244 | - [ ] Тестирование выхода/входа 245 | 246 | ### ✅ 3.5 Создание иконок 247 | - [ ] Профессиональные иконки с тематикой микрофона 248 | - [ ] Размеры: 16x16, 48x48, 128x128 249 | 250 | --- 251 | 252 | ## 🔒 Принципы безопасности (обновленные) 253 | 254 | ### ✅ Минимальные разрешения: 255 | - [x] `tabCapture` - только для записи аудио 256 | - [x] `activeTab` - только активная вкладка 257 | - [x] `notifications` - только уведомления 258 | - [x] **НЕТ `storage`** - никаких данных в браузере 259 | 260 | ### ✅ Безопасность данных: 261 | - [x] **Данные пользователя только в памяти** - очищаются после загрузки 262 | - [x] **Авторизация через cookie** - используем существующую сессию 263 | - [x] **Привязка к пользователю** - файлы содержат email 264 | - [x] **Проверка авторизации** - перед каждой записью 265 | 266 | ### ✅ Обработка авторизации: 267 | - [x] Проверка статуса при запуске 268 | - [x] Обработка ошибок 401 269 | - [x] Переход на веб-интерфейс для входа 270 | - [x] Очистка данных из памяти 271 | 272 | --- 273 | 274 | ## 🚀 Критерии готовности 275 | 276 | ### ✅ MVP (Минимально жизнеспособный продукт) 277 | - [x] Проверка авторизации работает 278 | - [x] Запись аудио встреч с привязкой к пользователю 279 | - [x] Загрузка на API с данными пользователя 280 | - [x] Уведомление с переходом в веб 281 | - [x] Обработка ошибок авторизации 282 | 283 | ### ✅ Полная версия 284 | - [x] Все функции MVP 285 | - [x] Определение типа встречи в метаданных 286 | - [x] Полированный UI для авторизации 287 | - [x] Обработка всех edge cases 288 | - [x] Профессиональные иконки 289 | 290 | --- 291 | 292 | ## 📝 Заметки для разработки 293 | 294 | ### Важные моменты: 295 | 1. **Простая авторизация** - только проверка статуса + переход в веб 296 | 2. **Безопасность** - данные пользователя только в памяти, никакого storage 297 | 3. **Привязка к пользователю** - каждая запись содержит email 298 | 4. **Обработка ошибок** - особенно 401 Unauthorized 299 | 5. **UX** - понятные состояния авторизации 300 | 301 | ### Флоу авторизации: 302 | 1. Открытие popup → проверка `/auth/status` 303 | 2. Если не авторизован → показать кнопку "Войти" 304 | 3. Клик "Войти" → открыть веб-интерфейс 305 | 4. После входа → пользователь возвращается к расширению 306 | 5. Повторная проверка статуса → показать секцию записи 307 | 308 | --- 309 | 310 | **Итого времени**: 2-3 дня активной разработки 311 | **Результат**: Простое и безопасное расширение с авторизацией 🎯 -------------------------------------------------------------------------------- /src/services/auth_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Сервис аутентификации с Google OAuth 3 | """ 4 | import os 5 | import json 6 | import uuid 7 | from datetime import datetime, timedelta 8 | from typing import Optional, Dict, Any 9 | from jose import JWTError, jwt 10 | from google.oauth2 import id_token 11 | from google.auth.transport import requests 12 | from google.auth.transport.requests import Request 13 | from google_auth_oauthlib.flow import Flow 14 | 15 | from ..config.settings import OAUTH_CONFIG, JWT_CONFIG 16 | from ..models.schemas import GoogleUser, User, UserSession, TokenData 17 | from ..services.database_service import DatabaseService 18 | 19 | 20 | class AuthService: 21 | """Сервис для работы с аутентификацией""" 22 | 23 | def __init__(self): 24 | """Инициализация сервиса аутентификации""" 25 | self.db_service = DatabaseService() 26 | self.secret_key = JWT_CONFIG['secret_key'] 27 | self.algorithm = JWT_CONFIG['algorithm'] 28 | self.access_token_expire_minutes = JWT_CONFIG['access_token_expire_minutes'] 29 | 30 | def create_access_token(self, data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: 31 | """ 32 | Создание JWT токена доступа 33 | 34 | Args: 35 | data: Данные для включения в токен 36 | expires_delta: Время жизни токена 37 | 38 | Returns: 39 | str: JWT токен 40 | """ 41 | to_encode = data.copy() 42 | 43 | if expires_delta: 44 | expire = datetime.utcnow() + expires_delta 45 | else: 46 | expire = datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes) 47 | 48 | to_encode.update({"exp": expire}) 49 | encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm) 50 | 51 | return encoded_jwt 52 | 53 | def verify_access_token(self, token: str) -> Optional[TokenData]: 54 | """ 55 | Проверка JWT токена 56 | 57 | Args: 58 | token: JWT токен для проверки 59 | 60 | Returns: 61 | TokenData: Данные из токена или None если токен невалиден 62 | """ 63 | try: 64 | payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm]) 65 | user_id: str = payload.get("sub") 66 | email: str = payload.get("email") 67 | 68 | if user_id is None: 69 | return None 70 | 71 | token_data = TokenData(user_id=user_id, email=email) 72 | return token_data 73 | 74 | except JWTError: 75 | return None 76 | 77 | def verify_google_token(self, token: str) -> Optional[GoogleUser]: 78 | """ 79 | Проверка Google ID токена 80 | 81 | Args: 82 | token: Google ID токен 83 | 84 | Returns: 85 | GoogleUser: Данные пользователя Google или None 86 | """ 87 | try: 88 | # Проверяем токен через Google API 89 | idinfo = id_token.verify_oauth2_token( 90 | token, 91 | requests.Request(), 92 | OAUTH_CONFIG['google_client_id'] 93 | ) 94 | 95 | # Проверяем, что токен от Google 96 | if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']: 97 | return None 98 | 99 | google_user = GoogleUser( 100 | email=idinfo['email'], 101 | name=idinfo['name'], 102 | picture=idinfo.get('picture'), 103 | google_id=idinfo['sub'], 104 | locale=idinfo.get('locale') 105 | ) 106 | 107 | return google_user 108 | 109 | except ValueError: 110 | # Токен невалиден 111 | return None 112 | 113 | def get_user_by_google_id(self, google_id: str) -> Optional[User]: 114 | """ 115 | Получение пользователя по Google ID 116 | 117 | Args: 118 | google_id: Google ID пользователя 119 | 120 | Returns: 121 | User: Пользователь или None 122 | """ 123 | users_data = self.db_service.get_users() 124 | 125 | for user_data in users_data: 126 | if user_data.get('google_id') == google_id: 127 | return User(**user_data) 128 | 129 | return None 130 | 131 | def get_user_by_id(self, user_id: str) -> Optional[User]: 132 | """ 133 | Получение пользователя по ID 134 | 135 | Args: 136 | user_id: ID пользователя 137 | 138 | Returns: 139 | User: Пользователь или None 140 | """ 141 | users_data = self.db_service.get_users() 142 | 143 | for user_data in users_data: 144 | if user_data.get('id') == user_id: 145 | return User(**user_data) 146 | 147 | return None 148 | 149 | def create_or_update_user(self, google_user: GoogleUser) -> User: 150 | """ 151 | Создание или обновление пользователя 152 | 153 | Args: 154 | google_user: Данные пользователя Google 155 | 156 | Returns: 157 | User: Созданный или обновленный пользователь 158 | """ 159 | # Проверяем, существует ли пользователь 160 | existing_user = self.get_user_by_google_id(google_user.google_id) 161 | 162 | current_time = datetime.utcnow() 163 | 164 | if existing_user: 165 | # Обновляем существующего пользователя 166 | user_data = { 167 | 'id': existing_user.id, 168 | 'email': google_user.email, 169 | 'name': google_user.name, 170 | 'picture': google_user.picture, 171 | 'google_id': google_user.google_id, 172 | 'created_at': existing_user.created_at, 173 | 'last_login': current_time, 174 | 'is_active': True 175 | } 176 | 177 | self.db_service.update_user(existing_user.id, user_data) 178 | 179 | else: 180 | # Создаем нового пользователя 181 | user_id = str(uuid.uuid4()) 182 | user_data = { 183 | 'id': user_id, 184 | 'email': google_user.email, 185 | 'name': google_user.name, 186 | 'picture': google_user.picture, 187 | 'google_id': google_user.google_id, 188 | 'created_at': current_time, 189 | 'last_login': current_time, 190 | 'is_active': True 191 | } 192 | 193 | self.db_service.create_user(user_data) 194 | 195 | return User(**user_data) 196 | 197 | def create_user_session(self, user_id: str, user_agent: Optional[str] = None, 198 | ip_address: Optional[str] = None) -> UserSession: 199 | """ 200 | Создание пользовательской сессии 201 | 202 | Args: 203 | user_id: ID пользователя 204 | user_agent: User-Agent браузера 205 | ip_address: IP адрес пользователя 206 | 207 | Returns: 208 | UserSession: Созданная сессия 209 | """ 210 | session_token = str(uuid.uuid4()) 211 | current_time = datetime.utcnow() 212 | expires_at = current_time + timedelta(minutes=self.access_token_expire_minutes) 213 | 214 | session_data = { 215 | 'user_id': user_id, 216 | 'session_token': session_token, 217 | 'expires_at': expires_at, 218 | 'created_at': current_time, 219 | 'user_agent': user_agent, 220 | 'ip_address': ip_address 221 | } 222 | 223 | self.db_service.create_user_session(session_data) 224 | 225 | return UserSession(**session_data) 226 | 227 | def get_google_auth_url(self, state: str) -> str: 228 | """ 229 | Получение URL для аутентификации через Google 230 | 231 | Args: 232 | state: Состояние для защиты от CSRF 233 | 234 | Returns: 235 | str: URL для аутентификации 236 | """ 237 | scopes = OAUTH_CONFIG['scopes'] # Используем scopes как есть, без добавления префикса 238 | 239 | auth_url = ( 240 | f"https://accounts.google.com/o/oauth2/auth?" 241 | f"client_id={OAUTH_CONFIG['google_client_id']}&" 242 | f"redirect_uri={OAUTH_CONFIG['redirect_uri']}&" 243 | f"scope={' '.join(scopes)}&" 244 | f"response_type=code&" 245 | f"state={state}&" 246 | f"access_type=offline&" 247 | f"prompt=consent" 248 | ) 249 | 250 | return auth_url 251 | 252 | def exchange_code_for_token(self, code: str) -> Optional[str]: 253 | """ 254 | Обмен кода авторизации на токен доступа 255 | 256 | Args: 257 | code: Код авторизации от Google 258 | 259 | Returns: 260 | str: ID токен или None при ошибке 261 | """ 262 | try: 263 | import requests as http_requests 264 | 265 | token_url = "https://oauth2.googleapis.com/token" 266 | data = { 267 | 'client_id': OAUTH_CONFIG['google_client_id'], 268 | 'client_secret': OAUTH_CONFIG['google_client_secret'], 269 | 'code': code, 270 | 'grant_type': 'authorization_code', 271 | 'redirect_uri': OAUTH_CONFIG['redirect_uri'] 272 | } 273 | 274 | response = http_requests.post(token_url, data=data) 275 | 276 | if response.status_code == 200: 277 | token_data = response.json() 278 | return token_data.get('id_token') 279 | else: 280 | return None 281 | 282 | except Exception as e: 283 | print(f"Ошибка при обмене кода на токен: {e}") 284 | return None -------------------------------------------------------------------------------- /web_interface/modules/api.js: -------------------------------------------------------------------------------- 1 | // Модуль для работы с API 2 | class ApiManager { 3 | constructor() { 4 | this.config = CONFIG.API; 5 | this.baseUrl = this.config.BASE_URL; 6 | // HF_TOKEN больше не передается из клиента - сервер берет его из переменных окружения 7 | } 8 | 9 | // Загрузка файла для транскрипции 10 | async uploadFile(file, options = {}) { 11 | const formData = new FormData(); 12 | formData.append('file', file); 13 | 14 | const params = new URLSearchParams({ 15 | model: options.model || CONFIG.TRANSCRIPTION.DEFAULT_MODEL, 16 | language: options.language || CONFIG.TRANSCRIPTION.DEFAULT_LANGUAGE, 17 | diarize: options.diarize || CONFIG.TRANSCRIPTION.DEFAULT_DIARIZE, 18 | // hf_token больше не передается - сервер автоматически использует токен из переменных окружения 19 | compute_type: options.compute_type || CONFIG.TRANSCRIPTION.DEFAULT_COMPUTE_TYPE, 20 | batch_size: options.batch_size || CONFIG.TRANSCRIPTION.DEFAULT_BATCH_SIZE 21 | }); 22 | 23 | const response = await fetch(`${this.baseUrl}${this.config.ENDPOINTS.UPLOAD}?${params}`, { 24 | method: 'POST', 25 | credentials: 'include', 26 | body: formData 27 | }); 28 | 29 | if (!response.ok) { 30 | throw new Error(`HTTP error! status: ${response.status}`); 31 | } 32 | 33 | return await response.json(); 34 | } 35 | 36 | // Получение статуса транскрипции 37 | async getStatus(taskId) { 38 | const response = await fetch(`${this.baseUrl}${this.config.ENDPOINTS.STATUS}/${taskId}`, { 39 | credentials: 'include' 40 | }); 41 | 42 | if (!response.ok) { 43 | throw new Error(`HTTP error! status: ${response.status}`); 44 | } 45 | 46 | return await response.json(); 47 | } 48 | 49 | // Получение списка транскрипций 50 | async getTranscriptions() { 51 | const url = `${this.baseUrl}${this.config.ENDPOINTS.TRANSCRIPTIONS}`; 52 | 53 | if (CONFIG.DEBUG && CONFIG.DEBUG.LOG_API_CALLS) { 54 | console.log(`API: Getting transcriptions from: ${url}`); 55 | } 56 | 57 | try { 58 | const response = await fetch(url, { 59 | credentials: 'include' 60 | }); 61 | 62 | if (!response.ok) { 63 | const errorText = await response.text(); 64 | console.error(`API Error: ${response.status} - ${errorText}`); 65 | throw new Error(`HTTP error! status: ${response.status} - ${errorText}`); 66 | } 67 | 68 | const data = await response.json(); 69 | 70 | if (CONFIG.DEBUG && CONFIG.DEBUG.LOG_API_CALLS) { 71 | console.log(`API: Got ${data.length} transcriptions`); 72 | } 73 | 74 | return data; 75 | } catch (error) { 76 | console.error('API: Failed to fetch transcriptions:', error); 77 | throw error; 78 | } 79 | } 80 | 81 | // Получение S3 ссылок 82 | async getS3Links(taskId) { 83 | const url = `${this.baseUrl}${this.config.ENDPOINTS.S3_LINKS}/${taskId}`; 84 | 85 | if (CONFIG.DEBUG && CONFIG.DEBUG.LOG_API_CALLS) { 86 | console.log(`[API] Получение S3 ссылок для задачи: ${taskId}`); 87 | console.log(`[API] URL запроса: ${url}`); 88 | } 89 | 90 | try { 91 | const response = await fetch(url, { 92 | credentials: 'include' 93 | }); 94 | 95 | if (!response.ok) { 96 | const errorText = await response.text(); 97 | console.error(`[API] Ошибка получения S3 ссылок: ${response.status} - ${errorText}`); 98 | throw new Error(`HTTP error! status: ${response.status} - ${errorText}`); 99 | } 100 | 101 | const data = await response.json(); 102 | 103 | if (CONFIG.DEBUG && CONFIG.DEBUG.LOG_API_CALLS) { 104 | console.log(`[API] Получены S3 ссылки:`, data); 105 | } 106 | 107 | // Возвращаем только s3_links из ответа 108 | return data.s3_links || {}; 109 | 110 | } catch (error) { 111 | console.error('[API] Ошибка при получении S3 ссылок:', error); 112 | throw error; 113 | } 114 | } 115 | 116 | // Скачивание файла 117 | async downloadFile(taskId, format) { 118 | let endpoint; 119 | 120 | // Логируем запрос если включен debug 121 | if (CONFIG.DEBUG && CONFIG.DEBUG.LOG_API_CALLS) { 122 | console.log(`API: Downloading file - TaskID: ${taskId}, Format: ${format}`); 123 | } 124 | 125 | switch (format) { 126 | case 'json': 127 | case 'docx': 128 | case 'pdf': 129 | endpoint = `${this.config.ENDPOINTS.DOWNLOAD_TRANSCRIPT}/${taskId}?format_type=${format}`; 130 | break; 131 | case 'srt': 132 | case 'vtt': 133 | case 'tsv': 134 | endpoint = `${this.config.ENDPOINTS.DOWNLOAD_SUBTITLE}/${taskId}?format_type=${format}`; 135 | break; 136 | default: 137 | throw new Error(`Неподдерживаемый формат: ${format}`); 138 | } 139 | 140 | const url = `${this.baseUrl}${endpoint}`; 141 | 142 | if (CONFIG.DEBUG && CONFIG.DEBUG.LOG_API_CALLS) { 143 | console.log(`API: Making request to: ${url}`); 144 | } 145 | 146 | const response = await fetch(url, { 147 | credentials: 'include' 148 | }); 149 | 150 | if (!response.ok) { 151 | const errorText = await response.text(); 152 | console.error(`API Error: ${response.status} - ${errorText}`); 153 | throw new Error(`HTTP error! status: ${response.status} - ${errorText}`); 154 | } 155 | 156 | return response; 157 | } 158 | 159 | // Скачивание аудио файла 160 | async downloadAudio(taskId) { 161 | const response = await fetch(`${this.baseUrl}${this.config.ENDPOINTS.DOWNLOAD_AUDIO}/${taskId}`, { 162 | credentials: 'include' 163 | }); 164 | 165 | if (!response.ok) { 166 | throw new Error(`HTTP error! status: ${response.status}`); 167 | } 168 | 169 | return response; 170 | } 171 | 172 | // Удаление транскрипции 173 | async deleteTranscription(taskId) { 174 | const response = await fetch(`${this.baseUrl}${this.config.ENDPOINTS.DELETE_TRANSCRIPTION}/${taskId}`, { 175 | method: 'DELETE', 176 | credentials: 'include' 177 | }); 178 | 179 | if (!response.ok) { 180 | throw new Error(`HTTP error! status: ${response.status}`); 181 | } 182 | 183 | return await response.json(); 184 | } 185 | 186 | // Отмена транскрипции 187 | async cancelTranscription(taskId) { 188 | const response = await fetch(`${this.baseUrl}${this.config.ENDPOINTS.STATUS}/${taskId}`, { 189 | method: 'DELETE', 190 | credentials: 'include' 191 | }); 192 | 193 | if (!response.ok) { 194 | throw new Error(`HTTP error! status: ${response.status}`); 195 | } 196 | 197 | return await response.json(); 198 | } 199 | 200 | // Загрузка файла с прогрессом 201 | downloadFileWithProgress(url, onProgress) { 202 | return new Promise((resolve, reject) => { 203 | const xhr = new XMLHttpRequest(); 204 | 205 | xhr.open('GET', url, true); 206 | xhr.responseType = 'blob'; 207 | 208 | xhr.onprogress = function(event) { 209 | if (event.lengthComputable && onProgress) { 210 | const percentComplete = (event.loaded / event.total) * 100; 211 | onProgress(percentComplete); 212 | } 213 | }; 214 | 215 | xhr.onload = function() { 216 | if (xhr.status === 200) { 217 | resolve(xhr.response); 218 | } else { 219 | reject(new Error(`HTTP error! status: ${xhr.status}`)); 220 | } 221 | }; 222 | 223 | xhr.onerror = function() { 224 | reject(new Error('Network error')); 225 | }; 226 | 227 | xhr.send(); 228 | }); 229 | } 230 | 231 | // Универсальный метод для API запросов 232 | async makeRequest(endpoint, options = {}) { 233 | const url = `${this.baseUrl}${endpoint}`; 234 | 235 | const defaultOptions = { 236 | credentials: 'include', 237 | headers: { 238 | 'Content-Type': 'application/json' 239 | } 240 | }; 241 | 242 | const requestOptions = { 243 | ...defaultOptions, 244 | ...options 245 | }; 246 | 247 | // Если передан body и это не FormData, конвертируем в JSON 248 | if (requestOptions.body && !(requestOptions.body instanceof FormData)) { 249 | requestOptions.body = JSON.stringify(requestOptions.body); 250 | } 251 | 252 | if (CONFIG.DEBUG && CONFIG.DEBUG.LOG_API_CALLS) { 253 | console.log(`[API] Making request to: ${url}`, requestOptions); 254 | } 255 | 256 | try { 257 | const response = await fetch(url, requestOptions); 258 | 259 | if (!response.ok) { 260 | const errorText = await response.text(); 261 | console.error(`[API] Error: ${response.status} - ${errorText}`); 262 | throw new Error(`HTTP error! status: ${response.status} - ${errorText}`); 263 | } 264 | 265 | const data = await response.json(); 266 | 267 | if (CONFIG.DEBUG && CONFIG.DEBUG.LOG_API_CALLS) { 268 | console.log(`[API] Response received:`, data); 269 | } 270 | 271 | return data; 272 | 273 | } catch (error) { 274 | console.error('[API] Request failed:', error); 275 | throw error; 276 | } 277 | } 278 | } 279 | 280 | // Экспорт для использования в других модулях 281 | window.ApiManager = ApiManager; -------------------------------------------------------------------------------- /src/services/summarization_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Сервис для суммаризации транскрипций 3 | """ 4 | import json 5 | import requests 6 | from typing import Dict, Any, Optional 7 | from ..config.settings import SUMMARIZATION_CONFIG 8 | import logging 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | class SummarizationService: 13 | """Сервис для создания суммаризации транскрипций""" 14 | 15 | def __init__(self): 16 | self.config = SUMMARIZATION_CONFIG 17 | 18 | async def create_summary(self, transcription_data: Dict[str, Any]) -> Dict[str, Any]: 19 | """ 20 | Создает суммаризацию на основе данных транскрипции 21 | 22 | Args: 23 | transcription_data: Данные транскрипции с сегментами 24 | 25 | Returns: 26 | Dict с результатом суммаризации 27 | """ 28 | try: 29 | logger.info("Начало создания суммаризации") 30 | 31 | # Извлекаем данные по спикерам 32 | speakers_data = self._extract_speaker_data(transcription_data) 33 | speaker_percentages = self._calculate_speaking_time(transcription_data) 34 | total_duration = self._calculate_total_duration(transcription_data) 35 | 36 | logger.info(f"Найдено спикеров: {len(speakers_data)}") 37 | logger.info(f"Общая продолжительность: {total_duration:.1f} минут") 38 | 39 | # Создаем промпт 40 | prompt = self._create_summarization_prompt(speakers_data, speaker_percentages, total_duration) 41 | logger.info(f"Размер промпта: {len(prompt)} символов") 42 | 43 | # Отправляем запрос к LLM API 44 | summary = await self._call_llm_api(prompt) 45 | 46 | logger.info("Суммаризация создана успешно") 47 | return summary 48 | 49 | except Exception as e: 50 | logger.error(f"Ошибка создания суммаризации: {e}") 51 | raise 52 | 53 | def _extract_speaker_data(self, transcription_data: Dict[str, Any]) -> Dict[str, list]: 54 | """Извлекает данные по спикерам из транскрипции""" 55 | speakers_data = {} 56 | 57 | segments = transcription_data.get('segments', []) 58 | for segment in segments: 59 | speaker = segment.get('speaker', 'UNKNOWN') 60 | text = segment.get('text', '').strip() 61 | 62 | if speaker not in speakers_data: 63 | speakers_data[speaker] = [] 64 | 65 | if text: 66 | speakers_data[speaker].append(text) 67 | 68 | return speakers_data 69 | 70 | def _calculate_speaking_time(self, transcription_data: Dict[str, Any]) -> Dict[str, float]: 71 | """Вычисляет время говорения каждого спикера в процентах""" 72 | speaker_times = {} 73 | total_time = 0 74 | 75 | segments = transcription_data.get('segments', []) 76 | for segment in segments: 77 | speaker = segment.get('speaker', 'UNKNOWN') 78 | duration = (segment.get('end', 0) - segment.get('start', 0)) 79 | 80 | if speaker not in speaker_times: 81 | speaker_times[speaker] = 0 82 | 83 | speaker_times[speaker] += duration 84 | total_time += duration 85 | 86 | # Конвертируем в проценты 87 | speaker_percentages = {} 88 | for speaker, time in speaker_times.items(): 89 | speaker_percentages[speaker] = round((time / total_time) * 100, 2) if total_time > 0 else 0 90 | 91 | return speaker_percentages 92 | 93 | def _calculate_total_duration(self, transcription_data: Dict[str, Any]) -> float: 94 | """Вычисляет общую продолжительность в минутах""" 95 | segments = transcription_data.get('segments', []) 96 | if segments: 97 | last_segment = segments[-1] 98 | return (last_segment.get('end', 0)) / 60 # в минутах 99 | return 0 100 | 101 | def _create_summarization_prompt(self, speakers_data: Dict[str, list], 102 | speaker_percentages: Dict[str, float], 103 | total_duration: float) -> str: 104 | """Создает промпт для суммаризации""" 105 | prompt = f"""Проанализируй следующую транскрипцию разговора и создай структурированное саммари. 106 | 107 | ИНФОРМАЦИЯ О РАЗГОВОРЕ: 108 | - Общая продолжительность: {total_duration:.1f} минут 109 | - Количество спикеров: {len(speakers_data)} 110 | 111 | ДАННЫЕ ПО СПИКЕРАМ: 112 | """ 113 | 114 | for speaker, texts in speakers_data.items(): 115 | percentage = speaker_percentages.get(speaker, 0) 116 | prompt += f"\n--- {speaker} (говорил {percentage}% времени) ---\n" 117 | prompt += "\n".join(texts[:10]) # Первые 10 фраз 118 | if len(texts) > 10: 119 | prompt += f"\n... и еще {len(texts) - 10} фраз" 120 | prompt += "\n" 121 | 122 | prompt += """ 123 | ЗАДАЧА: 124 | 1. Выбери оптимальную стратегию анализа (деловая встреча, интервью, лекция, дискуссия и т.д.) 125 | 2. Выдели ключевые моменты с временными метками 126 | 3. Проанализируй вклад каждого спикера 127 | 4. Создай финальное саммари с выводами 128 | 129 | Ответь в формате JSON согласно схеме.""" 130 | 131 | return prompt 132 | 133 | async def _call_llm_api(self, prompt: str) -> Dict[str, Any]: 134 | """Отправляет запрос к LLM API""" 135 | 136 | # JSON схема для structured output 137 | summarization_schema = { 138 | "type": "object", 139 | "properties": { 140 | "strategy_analysis": { 141 | "type": "object", 142 | "properties": { 143 | "chosen_strategy": {"type": "string"}, 144 | "reasoning": {"type": "string"}, 145 | "alternative_strategies": {"type": "array", "items": {"type": "string"}} 146 | }, 147 | "required": ["chosen_strategy", "reasoning", "alternative_strategies"] 148 | }, 149 | "key_milestones": { 150 | "type": "array", 151 | "items": { 152 | "type": "object", 153 | "properties": { 154 | "timestamp": {"type": "string"}, 155 | "speaker": {"type": "string"}, 156 | "milestone": {"type": "string"}, 157 | "importance": {"type": "string", "enum": ["high", "medium", "low"]} 158 | }, 159 | "required": ["timestamp", "speaker", "milestone", "importance"] 160 | } 161 | }, 162 | "speakers_analysis": { 163 | "type": "object", 164 | "additionalProperties": { 165 | "type": "object", 166 | "properties": { 167 | "main_topics": {"type": "array", "items": {"type": "string"}}, 168 | "speaking_time_percentage": {"type": "number"}, 169 | "key_points": {"type": "array", "items": {"type": "string"}} 170 | }, 171 | "required": ["main_topics", "speaking_time_percentage", "key_points"] 172 | } 173 | }, 174 | "final_summary": { 175 | "type": "object", 176 | "properties": { 177 | "overall_theme": {"type": "string"}, 178 | "main_discussion_points": {"type": "array", "items": {"type": "string"}}, 179 | "conclusions": {"type": "array", "items": {"type": "string"}}, 180 | "action_items": {"type": "array", "items": {"type": "string"}}, 181 | "duration_minutes": {"type": "number"} 182 | }, 183 | "required": ["overall_theme", "main_discussion_points", "conclusions", "action_items", "duration_minutes"] 184 | } 185 | }, 186 | "required": ["strategy_analysis", "key_milestones", "speakers_analysis", "final_summary"] 187 | } 188 | 189 | request_data = { 190 | "messages": [ 191 | { 192 | "role": "system", 193 | "content": "Ты - эксперт по анализу и суммаризации разговоров. Твоя задача - создать структурированное и информативное саммари транскрипции, выбрав оптимальную стратегию анализа и выделив ключевые моменты." 194 | }, 195 | { 196 | "role": "user", 197 | "content": prompt 198 | } 199 | ], 200 | "model": self.config['model'], 201 | "max_tokens": self.config['max_tokens'], 202 | "temperature": self.config['temperature'], 203 | "guided_json": json.dumps(summarization_schema), 204 | "guided_decoding_backend": "xgrammar", 205 | "frequency_penalty": 0, 206 | "presence_penalty": 0, 207 | "top_p": 0.9, 208 | "n": 1, 209 | "stream": False 210 | } 211 | 212 | logger.info(f"Отправка запроса к LLM API: {self.config['api_url']}") 213 | 214 | headers = { 215 | 'Content-Type': 'application/json', 216 | 'Authorization': f'Bearer {self.config["api_key"]}' 217 | } 218 | 219 | response = requests.post( 220 | self.config['api_url'], 221 | headers=headers, 222 | json=request_data, 223 | timeout=120 # 2 минуты таймаут 224 | ) 225 | 226 | logger.info(f"Ответ от LLM API получен, статус: {response.status_code}") 227 | 228 | if not response.ok: 229 | error_text = response.text 230 | logger.error(f"Ошибка LLM API: {response.status_code} - {error_text}") 231 | raise Exception(f"API error: {response.status_code} - {error_text}") 232 | 233 | result = response.json() 234 | 235 | if not result.get('choices') or not result['choices'][0].get('message'): 236 | logger.error(f"Неожиданная структура ответа: {result}") 237 | raise Exception('Неверная структура ответа от LLM API') 238 | 239 | summary_json = json.loads(result['choices'][0]['message']['content']) 240 | logger.info("Суммаризация успешно распарсена") 241 | 242 | return summary_json -------------------------------------------------------------------------------- /web_interface/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Вход - whisperx-fronted-docker-compose 7 | 8 | 195 | 196 | 197 |
198 | 203 | 204 |
205 |
206 | Войдите с помощью Google аккаунта для доступа к вашим персональным транскрипциям 207 |
208 | 209 | 213 | 214 |
215 | 216 | Безопасный вход через Google OAuth 2.0. Мы не сохраняем ваш пароль. 217 |
218 | 219 |
220 | 221 | Ошибка входа 222 |
223 | 224 |
225 | 226 | Вход выполнен успешно! Перенаправление... 227 |
228 |
229 | 230 | 233 |
234 | 235 | 279 | 280 | -------------------------------------------------------------------------------- /cursor-to-do/REALTIME_DEVELOPMENT_PLAN.md: -------------------------------------------------------------------------------- 1 | # whisperx-fronted-docker-compose Real-Time Transcription Development Plan 2 | 3 | ## 📋 Обзор проекта 4 | 5 | Добавление real-time транскрипции в существующий проект whisperx-fronted-docker-compose без нарушения текущего функционала. 6 | 7 | ### Цели: 8 | - ✅ Сохранить весь существующий функционал 9 | - ✅ Добавить кнопку real-time транскрипции на фронтенде 10 | - ✅ Использовать существующий WebSocket сервер 11 | - ✅ Интегрировать с WhisperX моделями 12 | - ✅ Обеспечить латентность < 500ms 13 | 14 | ### Архитектурный подход: 15 | - **Модульная разработка** - новые файлы без изменения существующих 16 | - **Опциональный функционал** - real-time как дополнительная возможность 17 | - **Переиспользование** - максимальное использование существующих сервисов 18 | 19 | --- 20 | 21 | ## 🏗️ Архитектура решения 22 | 23 | ```mermaid 24 | graph TB 25 | subgraph "Existing Frontend" 26 | A[index.html] 27 | B[main.js] 28 | C[transcription.js] 29 | end 30 | 31 | subgraph "New Real-Time Frontend" 32 | D[realtimeButton] 33 | E[realtimeAudio.js - NEW] 34 | F[realtimeUI.js - NEW] 35 | G[audioProcessor.js - NEW] 36 | end 37 | 38 | subgraph "Existing Backend" 39 | H[FastAPI Server :8880] 40 | I[WhisperX Manager] 41 | J[S3 Service] 42 | end 43 | 44 | subgraph "New Real-Time Backend" 45 | K[/ws/realtime - NEW] 46 | L[RealtimeManager - NEW] 47 | M[StreamingProcessor - NEW] 48 | end 49 | 50 | D --> E 51 | E --> F 52 | E --> G 53 | E --> K 54 | K --> L 55 | L --> M 56 | M --> I 57 | L --> J 58 | ``` 59 | 60 | --- 61 | 62 | ## 📁 Структура новых файлов 63 | 64 | ### Backend (новые файлы): 65 | ``` 66 | src/ 67 | ├── realtime/ # 🆕 Новая папка 68 | │ ├── __init__.py 69 | │ ├── manager.py # RealtimeTranscriptionManager 70 | │ ├── processor.py # StreamingAudioProcessor 71 | │ ├── websocket_handler.py # WebSocket обработчик 72 | │ └── models.py # Pydantic модели для real-time 73 | ├── api/ 74 | │ └── realtime_routes.py # 🆕 WebSocket маршруты 75 | ``` 76 | 77 | ### Frontend (новые файлы): 78 | ``` 79 | web_interface/ 80 | ├── modules/ 81 | │ ├── realtimeAudio.js # 🆕 Управление real-time аудио 82 | │ ├── realtimeUI.js # 🆕 UI для real-time режима 83 | │ └── audioProcessor.js # 🆕 AudioWorklet процессор 84 | ├── css/ 85 | │ └── realtime.css # 🆕 Стили для real-time UI 86 | ``` 87 | 88 | --- 89 | 90 | ## 🎯 План разработки по этапам 91 | 92 | ### Этап 1: Подготовка инфраструктуры (День 1-2) ✅ ЗАВЕРШЕН 93 | **Цель**: Создать базовую структуру без нарушения существующего кода 94 | 95 | #### 1.1 Создание новых директорий и файлов ✅ 96 | - [x] Создать `src/realtime/` директорию 97 | - [x] Создать базовые файлы с заглушками: 98 | - [x] `__init__.py` - модуль инициализации 99 | - [x] `models.py` - Pydantic модели и события 100 | - [x] `manager.py` - менеджер real-time транскрипции 101 | - [x] `processor.py` - процессор потокового аудио 102 | - [x] `websocket_handler.py` - WebSocket обработчик 103 | - [x] Создать API маршруты: 104 | - [x] `src/api/realtime_routes.py` - WebSocket эндпоинт и HTTP API 105 | - [x] Создать frontend модули: 106 | - [x] `web_interface/modules/realtimeAudio.js` - управление аудио и WebSocket 107 | - [x] `web_interface/modules/realtimeUI.js` - пользовательский интерфейс 108 | - [x] `web_interface/css/realtime.css` - стили для real-time UI 109 | - [x] Добавить real-time зависимости в requirements.txt 110 | 111 | #### 1.2 Минимальная интеграция 112 | - [ ] Добавить кнопку "Real-Time" в интерфейс 113 | - [ ] Интегрировать WebSocket эндпоинт в main.py 114 | - [ ] Протестировать подключение 115 | 116 | --- 117 | 118 | ### Этап 2: WebSocket инфраструктура (День 3-4) 119 | **Цель**: Настроить WebSocket соединение для real-time данных 120 | 121 | #### 2.1 Backend WebSocket 122 | ```python 123 | # src/api/realtime_routes.py - будет создан на этом этапе 124 | from fastapi import WebSocket, WebSocketDisconnect 125 | from src.realtime.websocket_handler import RealtimeWebSocketHandler 126 | 127 | @router.websocket("/ws/realtime") 128 | async def realtime_websocket(websocket: WebSocket): 129 | handler = RealtimeWebSocketHandler() 130 | await handler.handle_connection(websocket) 131 | ``` 132 | 133 | #### 2.2 Frontend WebSocket клиент 134 | ```javascript 135 | // web_interface/modules/realtimeAudio.js - будет создан на этом этапе 136 | class RealtimeAudioManager { 137 | constructor() { 138 | this.ws = null; 139 | this.isConnected = false; 140 | } 141 | 142 | async connect() { 143 | const wsUrl = `ws://localhost:8880/ws/realtime`; 144 | this.ws = new WebSocket(wsUrl); 145 | // Обработчики событий 146 | } 147 | } 148 | ``` 149 | 150 | --- 151 | 152 | ### Этап 3: Аудио захват и обработка (День 5-7) 153 | **Цель**: Реализовать захват микрофона и отправку аудио чанков 154 | 155 | #### 3.1 AudioWorklet процессор 156 | ```javascript 157 | // web_interface/modules/audioProcessor.js 158 | class RealtimeAudioProcessor extends AudioWorkletProcessor { 159 | constructor() { 160 | super(); 161 | this.sampleRate = 24000; 162 | this.chunkSizeMs = 100; // 100ms чанки 163 | this.buffer = []; 164 | } 165 | 166 | process(inputs, outputs, parameters) { 167 | // Обработка аудио и отправка чанков 168 | } 169 | } 170 | ``` 171 | 172 | #### 3.2 Интеграция с микрофоном 173 | ```javascript 174 | // Добавление в realtimeAudio.js 175 | async setupMicrophone() { 176 | const stream = await navigator.mediaDevices.getUserMedia({ 177 | audio: { 178 | sampleRate: 24000, 179 | channelCount: 1, 180 | echoCancellation: true, 181 | noiseSuppression: true 182 | } 183 | }); 184 | 185 | this.audioContext = new AudioContext({ sampleRate: 24000 }); 186 | // Настройка AudioWorklet 187 | } 188 | ``` 189 | 190 | --- 191 | 192 | ### Этап 4: Потоковая обработка на сервере (День 8-10) 193 | **Цель**: Интегрировать с WhisperX для потоковой транскрипции 194 | 195 | #### 4.1 Streaming Audio Processor 196 | ```python 197 | # src/realtime/processor.py 198 | class StreamingAudioProcessor: 199 | def __init__(self): 200 | self.buffer = AudioBuffer() 201 | self.whisper_manager = None # Использовать существующий 202 | 203 | async def process_chunk(self, audio_chunk: bytes) -> Optional[str]: 204 | # Добавить в буфер и обработать при достижении минимума 205 | pass 206 | 207 | async def get_partial_result(self) -> str: 208 | # Получить промежуточный результат 209 | pass 210 | ``` 211 | 212 | #### 4.2 Интеграция с существующим WhisperX 213 | ```python 214 | # src/realtime/manager.py 215 | from src.core.whisper_manager import WhisperManager 216 | 217 | class RealtimeTranscriptionManager: 218 | def __init__(self): 219 | self.whisper_manager = WhisperManager() # Переиспользуем существующий 220 | self.active_sessions = {} 221 | 222 | async def start_session(self, session_id: str): 223 | # Создать новую сессию 224 | pass 225 | ``` 226 | 227 | --- 228 | 229 | ### Этап 5: UI и пользовательский опыт (День 11-12) 230 | **Цель**: Создать интуитивный интерфейс для real-time режима 231 | 232 | #### 5.1 Real-time UI компоненты 233 | ```javascript 234 | // web_interface/modules/realtimeUI.js 235 | class RealtimeUI { 236 | constructor() { 237 | this.transcriptionArea = null; 238 | this.statusIndicator = null; 239 | this.volumeIndicator = null; 240 | } 241 | 242 | showRealtimeMode() { 243 | // Переключить интерфейс в real-time режим 244 | } 245 | 246 | updateTranscription(text, isFinal = false) { 247 | // Обновить текст транскрипции 248 | } 249 | } 250 | ``` 251 | 252 | #### 5.2 Интеграция с существующим UI 253 | ```javascript 254 | // Минимальные изменения в web_interface/modules/main.js 255 | // Добавить только обработчик кнопки real-time 256 | document.getElementById('realtime-btn').addEventListener('click', () => { 257 | if (window.realtimeManager) { 258 | window.realtimeManager.toggle(); 259 | } 260 | }); 261 | ``` 262 | 263 | --- 264 | 265 | ### Этап 6: Оптимизация и тестирование (День 13-14) 266 | **Цель**: Оптимизировать производительность и протестировать 267 | 268 | #### 6.1 Оптимизация буферизации 269 | - [ ] Настроить размеры буферов 270 | - [ ] Оптимизировать частоту отправки чанков 271 | - [ ] Добавить адаптивное качество 272 | 273 | #### 6.2 Тестирование 274 | - [ ] Тест латентности 275 | - [ ] Тест качества транскрипции 276 | - [ ] Тест стабильности соединения 277 | - [ ] Тест совместимости браузеров 278 | 279 | --- 280 | 281 | ## 🔧 Технические детали 282 | 283 | ### WebSocket события 284 | ```javascript 285 | // События клиент -> сервер 286 | { 287 | "type": "session.start", 288 | "config": { 289 | "language": "ru", 290 | "model": "large-v3" 291 | } 292 | } 293 | 294 | { 295 | "type": "audio.chunk", 296 | "data": "base64_audio_data", 297 | "sequence": 123 298 | } 299 | 300 | // События сервер -> клиент 301 | { 302 | "type": "transcription.partial", 303 | "text": "промежуточный текст...", 304 | "confidence": 0.85 305 | } 306 | 307 | { 308 | "type": "transcription.final", 309 | "text": "финальный текст", 310 | "confidence": 0.95 311 | } 312 | ``` 313 | 314 | ### Аудио параметры 315 | - **Sample Rate**: 24kHz (совместимо с WhisperX) 316 | - **Channels**: 1 (моно) 317 | - **Format**: PCM16 318 | - **Chunk Size**: 100ms (2400 samples) 319 | - **Buffer Size**: 1-3 секунды для обработки 320 | 321 | --- 322 | 323 | ## 🎛️ Настройки и конфигурация 324 | 325 | ### Новые настройки в config 326 | ```python 327 | # src/config/settings.py - добавить новые параметры 328 | REALTIME_ENABLED: bool = True 329 | REALTIME_CHUNK_SIZE_MS: int = 100 330 | REALTIME_BUFFER_SIZE_MS: int = 1000 331 | REALTIME_MAX_SESSIONS: int = 10 332 | REALTIME_LATENCY_TARGET_MS: int = 500 333 | ``` 334 | 335 | ### Frontend конфигурация 336 | ```javascript 337 | // web_interface/config.js - добавить real-time настройки 338 | const REALTIME_CONFIG = { 339 | enabled: true, 340 | chunkSizeMs: 100, 341 | sampleRate: 24000, 342 | maxLatencyMs: 500, 343 | autoStart: false 344 | }; 345 | ``` 346 | 347 | --- 348 | 349 | ## 📊 Метрики и мониторинг 350 | 351 | ### Ключевые метрики 352 | - **Латентность**: время от речи до отображения текста 353 | - **Точность**: качество распознавания речи 354 | - **Стабильность**: процент успешных сессий 355 | - **Производительность**: использование CPU/памяти 356 | 357 | ### Логирование 358 | ```python 359 | # Добавить в существующую систему логирования 360 | logger.info(f"Realtime session started: {session_id}") 361 | logger.debug(f"Audio chunk processed: {chunk_size}ms, latency: {latency}ms") 362 | logger.error(f"Realtime session error: {error}") 363 | ``` 364 | 365 | --- 366 | 367 | ## 🚀 Следующие шаги 368 | 369 | 1. **Создать базовую структуру файлов** (Этап 1) 370 | 2. **Настроить WebSocket соединение** (Этап 2) 371 | 3. **Реализовать аудио захват** (Этап 3) 372 | 4. **Интегрировать с WhisperX** (Этап 4) 373 | 5. **Создать UI** (Этап 5) 374 | 6. **Оптимизировать и тестировать** (Этап 6) 375 | 376 | ### Готов начать с Этапа 1? 377 | Создадим базовую структуру файлов и добавим кнопку в интерфейс! -------------------------------------------------------------------------------- /web_interface/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AI-Transcribe - Транскрипция аудио и видео 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 |

whisperx-fronted-docker-compose - AI Транскрипция

17 |

Персональная система транскрипции с автоматической загрузкой на S3 - Загрузите аудио или видео файл для получения транскрипции

18 |
19 |
20 | 30 |
31 | 34 |
35 |
36 |
37 | 38 | 39 |
40 |
41 |
42 | 43 |

Перетащите файл сюда или нажмите для выбора

44 |

Поддерживаются: MP3, M4A, WAV, MP4, AVI, MKV и другие
45 | Экспорт в 6 форматах: JSON, SRT, VTT, TSV, DOCX, PDF
46 | ☁️ Автоматическая загрузка на Yandex Cloud S3

47 | 48 |
49 | 52 | 55 |
56 |
57 |
58 | 59 | 60 | 101 |
102 | 103 | 104 | 122 | 123 | 124 | 182 | 183 | 184 |
185 |

История транскрипций

186 |
187 | 188 |
189 |
190 |
191 | 192 | 193 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | --------------------------------------------------------------------------------