#!/bin/bash set -e # --- НАСТРОЙКИ (ПРОВЕРЬТЕ ПЕРЕД ЗАПУСКОМ) --- WEB_HOST="192.168.254.36" DB_HOST="192.168.254.35" DB_PORT="5432" DB_NAME="asr_db" DB_USER="asr_user" DB_PASSWORD="poluyan77" ASR_HOST="192.168.254.34" ASR_AUDIO_PORT="8080" ASR_CMD_PORT="8003" ADMIN_PASSWORD="admin123" # --- КОНЕЦ НАСТРОЕК --- echo "=== 1. Установка базовых пакетов ===" apt update apt install -y python3 python3-pip python3-venv nginx curl git echo "=== 2. Установка Node.js для сборки фронтенда ===" curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt install -y nodejs echo "=== 3. Создание проекта и виртуального окружения ===" mkdir -p /opt/asr-web cd /opt/asr-web python3 -m venv venv source venv/bin/activate echo "=== 4. Установка Python-пакетов ===" pip install fastapi uvicorn psycopg2-binary python-jose[cryptography] passlib[bcrypt] python-multipart bcrypt requests echo "=== 5. Создание API сервера (main.py) с переменными окружения ===" cat > /opt/asr-web/main.py << 'EOF' import os from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from pydantic import BaseModel from typing import List, Optional from datetime import datetime, timedelta import psycopg2 from psycopg2.extras import RealDictCursor from jose import JWTError, jwt import bcrypt import requests # ========== НАСТРОЙКИ ИЗ ПЕРЕМЕННЫХ ОКРУЖЕНИЯ ========== DB_HOST = os.getenv("DB_HOST", "192.168.254.35") DB_PORT = os.getenv("DB_PORT", "5432") DB_NAME = os.getenv("DB_NAME", "asr_db") DB_USER = os.getenv("DB_USER", "asr_user") DB_PASSWORD = os.getenv("DB_PASSWORD", "poluyan77") ASR_HOST = os.getenv("ASR_HOST", "192.168.254.34") ASR_AUDIO_PORT = os.getenv("ASR_AUDIO_PORT", "8080") ASR_CMD_PORT = os.getenv("ASR_CMD_PORT", "8003") SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-this-in-production-12345") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 480 DB_CONFIG = { "host": DB_HOST, "port": DB_PORT, "database": DB_NAME, "user": DB_USER, "password": DB_PASSWORD } security = HTTPBearer() app = FastAPI(title="ASR API", version="1.0") # ========== МОДЕЛИ ========== class UserLogin(BaseModel): username: str password: str class Token(BaseModel): access_token: str token_type: str class CallResponse(BaseModel): id: int audio_filename: str incoming_number: Optional[str] = None outgoing_number: Optional[str] = None call_date: Optional[str] = None call_time: Optional[str] = None operator_name: Optional[str] = None status: str duration: Optional[float] = None class TagInput(BaseModel): tag_name: str class OperatorNameInput(BaseModel): name: str class CommentInput(BaseModel): comment: str # ========== ФУНКЦИИ ========== def get_db(): conn = psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor) try: yield conn finally: conn.close() def verify_password(plain_password, hashed_password): return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8')) def authenticate_user(username: str, password: str, conn): cursor = conn.cursor() cursor.execute("SELECT id, username, password_hash, role FROM users WHERE username = %s", (username,)) user = cursor.fetchone() if not user: return None if not verify_password(password, user['password_hash']): return None return user def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) to_encode.update({"exp": expire}) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security), conn = Depends(get_db)): token = credentials.credentials try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise HTTPException(status_code=401, detail="Invalid token") except JWTError: raise HTTPException(status_code=401, detail="Invalid token") cursor = conn.cursor() cursor.execute("SELECT id, username, role FROM users WHERE username = %s", (username,)) user = cursor.fetchone() if user is None: raise HTTPException(status_code=401, detail="User not found") return user # ========== ЭНДПОИНТЫ ========== @app.post("/api/token", response_model=Token) async def login(form: UserLogin, conn = Depends(get_db)): user = authenticate_user(form.username, form.password, conn) if not user: raise HTTPException(status_code=401, detail="Invalid credentials") access_token = create_access_token(data={"sub": user['username']}) return {"access_token": access_token, "token_type": "bearer"} @app.get("/api/calls", response_model=List[CallResponse]) async def get_calls( skip: int = 0, limit: int = 50, search: Optional[str] = None, conn = Depends(get_db), current_user = Depends(get_current_user) ): cursor = conn.cursor() if search: cursor.execute(""" SELECT DISTINCT c.id, c.audio_filename, c.incoming_number, c.outgoing_number, c.call_date, c.call_time, c.operator_name, c.status, c.audio_duration_sec as duration FROM calls c JOIN transcriptions t ON t.call_id = c.id JOIN transcription_segments s ON s.transcript_id = t.id WHERE to_tsvector('russian', s.text) @@ plainto_tsquery('russian', %s) ORDER BY c.call_date DESC LIMIT %s OFFSET %s """, (search, limit, skip)) else: cursor.execute(""" SELECT id, audio_filename, incoming_number, outgoing_number, call_date, call_time, operator_name, status, audio_duration_sec as duration FROM calls ORDER BY call_date DESC LIMIT %s OFFSET %s """, (limit, skip)) rows = cursor.fetchall() result = [] for row in rows: item = dict(row) if item.get('call_date'): item['call_date'] = str(item['call_date']) if item.get('call_time'): item['call_time'] = str(item['call_time']) result.append(item) return result @app.get("/api/calls/{call_id}/transcription") async def get_transcription(call_id: int, conn = Depends(get_db), current_user = Depends(get_current_user)): cursor = conn.cursor() cursor.execute(""" SELECT s.start_time_sec, s.end_time_sec, s.speaker, s.text FROM transcription_segments s JOIN transcriptions t ON t.id = s.transcript_id WHERE t.call_id = %s ORDER BY s.start_time_sec """, (call_id,)) segments = cursor.fetchall() cursor.execute("SELECT full_text FROM transcriptions WHERE call_id = %s", (call_id,)) full_text = cursor.fetchone() return { "call_id": call_id, "full_text": full_text['full_text'] if full_text else "", "segments": [dict(seg) for seg in segments] } @app.get("/api/calls/{call_id}/audio") async def get_audio_url(call_id: int, conn = Depends(get_db), current_user = Depends(get_current_user)): cursor = conn.cursor() cursor.execute("SELECT audio_filename FROM calls WHERE id = %s", (call_id,)) row = cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Call not found") filename = row['audio_filename'].replace('.wav', '.flac') audio_url = f"http://{ASR_HOST}:{ASR_AUDIO_PORT}/audio/{filename}" return {"audio_url": audio_url} @app.post("/api/calls/{call_id}/tags") async def add_tag(call_id: int, tag: TagInput, conn = Depends(get_db), current_user = Depends(get_current_user)): cursor = conn.cursor() cursor.execute("SELECT 1 FROM auto_tags WHERE call_id = %s AND tag_name = %s", (call_id, tag.tag_name)) if cursor.fetchone(): raise HTTPException(status_code=400, detail="Tag already exists") cursor.execute("INSERT INTO auto_tags (call_id, tag_name, confidence) VALUES (%s, %s, 1.0)", (call_id, tag.tag_name)) conn.commit() return {"message": "Tag added", "tag": tag.tag_name} @app.delete("/api/calls/{call_id}/tags/{tag_name}") async def delete_tag(call_id: int, tag_name: str, conn = Depends(get_db), current_user = Depends(get_current_user)): cursor = conn.cursor() cursor.execute("DELETE FROM auto_tags WHERE call_id = %s AND tag_name = %s AND tag_name NOT LIKE 'эмоция_%'", (call_id, tag_name)) conn.commit() if cursor.rowcount == 0: raise HTTPException(status_code=404, detail="Tag not found") return {"status": "ok", "deleted": tag_name} @app.get("/api/calls/{call_id}/tags") async def get_tags(call_id: int, conn = Depends(get_db), current_user = Depends(get_current_user)): cursor = conn.cursor() cursor.execute("SELECT tag_name, confidence, created_at FROM auto_tags WHERE call_id = %s ORDER BY created_at DESC", (call_id,)) tags = cursor.fetchall() return {"tags": [dict(tag) for tag in tags]} @app.get("/api/calls/{call_id}/comment") async def get_comment(call_id: int, conn = Depends(get_db), current_user = Depends(get_current_user)): cursor = conn.cursor() cursor.execute("SELECT quality_comment FROM calls WHERE id = %s", (call_id,)) row = cursor.fetchone() return {"comment": row['quality_comment'] if row else ""} @app.post("/api/calls/{call_id}/comment") async def save_comment(call_id: int, data: CommentInput, conn = Depends(get_db), current_user = Depends(get_current_user)): cursor = conn.cursor() cursor.execute("UPDATE calls SET quality_comment = %s WHERE id = %s", (data.comment, call_id)) conn.commit() return {"status": "ok"} # ========== УПРАВЛЕНИЕ ИМЕНАМИ ОПЕРАТОРОВ ========== @app.get("/api/operator-names") async def get_operator_names(conn = Depends(get_db), current_user = Depends(get_current_user)): cursor = conn.cursor() cursor.execute("SELECT id, name FROM operator_names ORDER BY name") rows = cursor.fetchall() return [{"id": row['id'], "name": row['name']} for row in rows] @app.post("/api/operator-names") async def add_operator_name(data: OperatorNameInput, conn = Depends(get_db), current_user = Depends(get_current_user)): if current_user['role'] != 'admin': raise HTTPException(status_code=403, detail="Только для администратора") cursor = conn.cursor() try: cursor.execute("INSERT INTO operator_names (name) VALUES (%s) RETURNING id", (data.name,)) result = cursor.fetchone() conn.commit() return {"id": result[0], "name": data.name} except Exception: conn.rollback() raise HTTPException(status_code=400, detail="Имя уже существует") @app.delete("/api/operator-names/{name_id}") async def delete_operator_name(name_id: int, conn = Depends(get_db), current_user = Depends(get_current_user)): if current_user['role'] != 'admin': raise HTTPException(status_code=403, detail="Только для администратора") cursor = conn.cursor() cursor.execute("DELETE FROM operator_names WHERE id = %s", (name_id,)) conn.commit() return {"status": "ok"} @app.post("/api/operator-names/refresh-cache") async def refresh_operator_names_cache(current_user = Depends(get_current_user)): if current_user['role'] != 'admin': raise HTTPException(status_code=403, detail="Только для администратора") try: response = requests.post(f'http://{ASR_HOST}:{ASR_CMD_PORT}/reload-names', timeout=5) if response.status_code == 200: data = response.json() return {"status": "ok", "message": f"Кэш обновлён, загружено {data.get('count', 0)} имён"} else: return {"status": "warning", "message": "Воркер не ответил"} except Exception as e: return {"status": "warning", "message": f"Ошибка: {e}"} @app.get("/api/health") async def health(): return {"status": "ok"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) EOF echo "=== 6. Создание systemd сервиса для API с переменными окружения ===" cat > /etc/systemd/system/asr-api.service << EOF [Unit] Description=ASR FastAPI After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/asr-web Environment="DB_HOST=$DB_HOST" Environment="DB_PORT=$DB_PORT" Environment="DB_NAME=$DB_NAME" Environment="DB_USER=$DB_USER" Environment="DB_PASSWORD=$DB_PASSWORD" Environment="ASR_HOST=$ASR_HOST" Environment="ASR_AUDIO_PORT=$ASR_AUDIO_PORT" Environment="ASR_CMD_PORT=$ASR_CMD_PORT" ExecStart=/opt/asr-web/venv/bin/python /opt/asr-web/main.py Restart=always [Install] WantedBy=multi-user.target EOF echo "=== 7. Настройка Nginx ===" cat > /etc/nginx/sites-available/asr-frontend << EOF server { listen 80; server_name $WEB_HOST; root /opt/asr-web/frontend/dist; index index.html; location / { try_files \$uri \$uri/ /index.html; } location /api/ { proxy_pass http://localhost:8000/api/; proxy_set_header Host \$host; } location /audio/ { proxy_pass http://$ASR_HOST:$ASR_AUDIO_PORT/audio/; add_header Access-Control-Allow-Origin *; } } EOF ln -sf /etc/nginx/sites-available/asr-frontend /etc/nginx/sites-enabled/ rm -f /etc/nginx/sites-enabled/default nginx -t systemctl restart nginx echo "=== 8. Базовый фронтенд (требуется копирование src из работающей ВМ) ===" cd /opt/asr-web npm create vite@5 frontend -- --template vue cd frontend npm install npm install vuetify@3 axios vue-router @mdi/font chart.js mkdir -p src/plugins cat > src/plugins/vuetify.js << 'EOF' import 'vuetify/styles' import { createVuetify } from 'vuetify' import * as components from 'vuetify/components' import * as directives from 'vuetify/directives' import '@mdi/font/css/materialdesignicons.css' export default createVuetify({ components, directives, theme: { defaultTheme: 'light', themes: { light: { colors: { primary: '#1976D2', secondary: '#424242', accent: '#82B1FF', }, }, }, }, }) EOF cat > src/main.js << 'EOF' import { createApp } from 'vue' import App from './App.vue' import vuetify from './plugins/vuetify' createApp(App).use(vuetify).mount('#app') EOF # Базовый App.vue (минимальный) cat > src/App.vue << 'EOF' ASR Аналитика Для полноценной работы скопируйте компоненты из работающей ВМ в папку /opt/asr-web/frontend/src/components/ Затем выполните npm run build EOF echo "=== 9. Запуск сервисов ===" systemctl daemon-reload systemctl enable asr-api systemctl start asr-api systemctl restart nginx echo "=== Web ВМ готова ===" echo "API: http://$WEB_HOST:8000" echo "Frontend: http://$WEB_HOST" echo "" echo "⚠️ ВАЖНО: Для полноценной работы фронтенда скопируйте компоненты из работающей ВМ:" echo " /opt/asr-web/frontend/src/components/" echo " /opt/asr-web/frontend/src/App.vue (полную версию)" echo " /opt/asr-web/frontend/src/main.js (с импортами всех компонентов)" echo "" echo "Затем выполните: cd /opt/asr-web/frontend && npm run build"
Для полноценной работы скопируйте компоненты из работающей ВМ в папку /opt/asr-web/frontend/src/components/
Затем выполните npm run build