Files
Transcryption-phone/setup_web.sh
T
2026-06-02 13:17:41 +00:00

462 lines
16 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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'
<template>
<v-app>
<v-main>
<v-container>
<v-card>
<v-card-title>ASR Аналитика</v-card-title>
<v-card-text>
<p>Для полноценной работы скопируйте компоненты из работающей ВМ в папку /opt/asr-web/frontend/src/components/</p>
<p>Затем выполните npm run build</p>
</v-card-text>
</v-card>
</v-container>
</v-main>
</v-app>
</template>
<script>
export default {
data() {
return {}
}
}
</script>
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"