Skip to content

Commit edb43de

Browse files
authored
Merge pull request #572 from ShaerWare/server/phase-4.5-admin-endpoints
Phase 4.5: Extract all remaining admin endpoints from orchestrator
2 parents 422ccf6 + 4109bd1 commit edb43de

4 files changed

Lines changed: 394 additions & 1373 deletions

File tree

modules/llm/router_models.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""HuggingFace model management endpoints.
2+
3+
List, scan, download, delete, and search models.
4+
GPU-only — not registered in cloud deployment mode.
5+
"""
6+
7+
import logging
8+
9+
from fastapi import APIRouter, HTTPException, Request
10+
11+
from model_manager import get_model_manager
12+
13+
14+
logger = logging.getLogger(__name__)
15+
16+
router = APIRouter(prefix="/admin/models", tags=["models"])
17+
18+
19+
@router.get("/list")
20+
async def admin_list_models():
21+
"""Список всех локальных моделей"""
22+
manager = get_model_manager()
23+
return {"models": manager.get_cached_models()}
24+
25+
26+
@router.post("/scan")
27+
async def admin_scan_models(request: Request):
28+
"""Запустить сканирование моделей"""
29+
data = await request.json() if request.headers.get("content-type") == "application/json" else {}
30+
include_system = data.get("include_system", False)
31+
32+
manager = get_model_manager()
33+
if manager.scan_all_models(include_system=include_system):
34+
return {"status": "ok", "message": "Scan started"}
35+
else:
36+
return {"status": "error", "message": "Scan already in progress"}
37+
38+
39+
@router.post("/scan/cancel")
40+
async def admin_cancel_scan():
41+
"""Отменить сканирование"""
42+
manager = get_model_manager()
43+
manager.cancel_scan()
44+
return {"status": "ok", "message": "Scan cancelled"}
45+
46+
47+
@router.get("/scan/status")
48+
async def admin_scan_status():
49+
"""Статус сканирования"""
50+
manager = get_model_manager()
51+
return {"status": manager.get_scan_progress()}
52+
53+
54+
@router.post("/download")
55+
async def admin_download_model(request: Request):
56+
"""Скачать модель с HuggingFace"""
57+
data = await request.json()
58+
repo_id = data.get("repo_id")
59+
revision = data.get("revision", "main")
60+
61+
if not repo_id:
62+
raise HTTPException(status_code=400, detail="repo_id required")
63+
64+
manager = get_model_manager()
65+
if manager.download_model(repo_id, revision):
66+
return {"status": "ok", "message": f"Download started: {repo_id}"}
67+
else:
68+
return {"status": "error", "message": "Download already in progress"}
69+
70+
71+
@router.post("/download/cancel")
72+
async def admin_cancel_download():
73+
"""Отменить загрузку"""
74+
manager = get_model_manager()
75+
manager.cancel_download()
76+
return {"status": "ok", "message": "Download cancelled"}
77+
78+
79+
@router.get("/download/status")
80+
async def admin_download_status():
81+
"""Статус загрузки"""
82+
manager = get_model_manager()
83+
return {"status": manager.get_download_progress()}
84+
85+
86+
@router.delete("/delete")
87+
async def admin_delete_model(path: str):
88+
"""Удалить модель"""
89+
manager = get_model_manager()
90+
result = manager.delete_model(path)
91+
if result["status"] == "ok":
92+
return result
93+
else:
94+
raise HTTPException(status_code=400, detail=result.get("error", "Delete failed"))
95+
96+
97+
@router.get("/search")
98+
async def admin_search_huggingface(query: str, limit: int = 20):
99+
"""Поиск моделей на HuggingFace"""
100+
manager = get_model_manager()
101+
results = manager.search_huggingface(query, limit)
102+
return {"results": results}
103+
104+
105+
@router.get("/details/{repo_id:path}")
106+
async def admin_get_model_details(repo_id: str):
107+
"""Получить детали модели с HuggingFace"""
108+
manager = get_model_manager()
109+
details = manager.get_model_details(repo_id)
110+
if details:
111+
return {"details": details}
112+
else:
113+
raise HTTPException(status_code=404, detail="Model not found")

modules/monitoring/router_logs.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Log viewing endpoints.
2+
3+
List, read, and stream application logs.
4+
"""
5+
6+
import logging
7+
from typing import Optional
8+
9+
from fastapi import APIRouter, Depends
10+
from fastapi.responses import StreamingResponse
11+
12+
from auth_manager import User, require_permission
13+
from service_manager import get_service_manager
14+
15+
16+
logger = logging.getLogger(__name__)
17+
18+
router = APIRouter(prefix="/admin/logs", tags=["logs"])
19+
20+
21+
@router.get("")
22+
async def admin_list_logs():
23+
"""Список доступных логов"""
24+
manager = get_service_manager()
25+
return {"logs": manager.get_available_logs()}
26+
27+
28+
@router.get("/{logfile}")
29+
async def admin_read_log(
30+
logfile: str, lines: int = 100, offset: int = 0, search: Optional[str] = None
31+
):
32+
"""Прочитать лог файл"""
33+
manager = get_service_manager()
34+
return manager.read_log(logfile, lines=lines, offset=offset, search=search)
35+
36+
37+
@router.get("/stream/{logfile}")
38+
async def admin_stream_log(
39+
logfile: str,
40+
user: User = Depends(require_permission("system", "view")),
41+
):
42+
"""SSE streaming логов"""
43+
manager = get_service_manager()
44+
45+
async def generate():
46+
async for data in manager.stream_log(logfile):
47+
yield f"data: {data}\n\n"
48+
49+
return StreamingResponse(
50+
generate(),
51+
media_type="text/event-stream",
52+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
53+
)

modules/speech/router_voices.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""Voice selection and testing endpoints.
2+
3+
Manages active voice configuration (XTTS/OpenVoice/Piper).
4+
GPU-only — not registered in cloud deployment mode.
5+
"""
6+
7+
import logging
8+
import time
9+
from pathlib import Path
10+
11+
from fastapi import APIRouter, HTTPException
12+
from fastapi.responses import FileResponse
13+
from pydantic import BaseModel
14+
15+
from app.dependencies import get_container
16+
17+
18+
logger = logging.getLogger(__name__)
19+
20+
router = APIRouter(tags=["voices"])
21+
22+
TEMP_DIR = Path("./temp")
23+
TEMP_DIR.mkdir(exist_ok=True)
24+
25+
26+
class VoiceRequest(BaseModel):
27+
voice: str # anna / marina / marina_openvoice / dmitri / irina
28+
29+
30+
@router.get("/admin/voices")
31+
async def admin_get_voices():
32+
"""Получить список всех доступных голосов"""
33+
container = get_container()
34+
voices = []
35+
36+
# XTTS голос (Анна) - требует GPU CC >= 7.0 (по умолчанию)
37+
if container.anna_voice_service:
38+
voices.append(
39+
{
40+
"id": "anna",
41+
"name": "Анна (XTTS)",
42+
"engine": "xtts",
43+
"description": "Клонированный голос Гули (XTTS v2, GPU CC >= 7.0)",
44+
"available": True,
45+
"samples_count": len(container.anna_voice_service.voice_samples),
46+
"default": True,
47+
}
48+
)
49+
50+
# XTTS голос (Марина) - требует GPU CC >= 7.0
51+
if container.voice_service:
52+
voices.append(
53+
{
54+
"id": "marina",
55+
"name": "Марина (XTTS)",
56+
"engine": "xtts",
57+
"description": "Клонированный голос Лидии (XTTS v2, GPU CC >= 7.0)",
58+
"available": True,
59+
"samples_count": len(container.voice_service.voice_samples),
60+
}
61+
)
62+
63+
# OpenVoice голос (Марина) - работает на GPU CC 6.1+
64+
if container.openvoice_service:
65+
voices.append(
66+
{
67+
"id": "marina_openvoice",
68+
"name": "Марина (OpenVoice)",
69+
"engine": "openvoice",
70+
"description": "Клонированный голос (OpenVoice v2, GPU CC 6.1+)",
71+
"available": True,
72+
"samples_count": len(container.openvoice_service.voice_samples)
73+
if container.openvoice_service.voice_samples
74+
else 0,
75+
}
76+
)
77+
78+
# Piper голоса (CPU)
79+
if container.piper_service:
80+
piper_voices = container.piper_service.get_available_voices()
81+
for voice_id, info in piper_voices.items():
82+
voices.append(
83+
{
84+
"id": voice_id,
85+
"name": info["name"],
86+
"engine": "piper",
87+
"description": info["description"],
88+
"available": info["available"],
89+
}
90+
)
91+
92+
return {
93+
"voices": voices,
94+
"current": container.current_voice_config,
95+
}
96+
97+
98+
@router.get("/admin/voice")
99+
async def admin_get_current_voice():
100+
"""Получить текущий выбранный голос"""
101+
container = get_container()
102+
return container.current_voice_config
103+
104+
105+
@router.post("/admin/voice")
106+
async def admin_set_voice(request: VoiceRequest):
107+
"""Установить активный голос"""
108+
container = get_container()
109+
voice_id = request.voice.lower()
110+
111+
if voice_id == "anna":
112+
if not container.anna_voice_service:
113+
raise HTTPException(
114+
status_code=503, detail="XTTS service (Анна) not available (requires GPU CC >= 7.0)"
115+
)
116+
new_config = {"engine": "xtts", "voice": "anna"}
117+
logger.info("🎤 Голос изменён на: Анна (XTTS)")
118+
119+
elif voice_id == "marina":
120+
if not container.voice_service:
121+
raise HTTPException(
122+
status_code=503,
123+
detail="XTTS service (Марина) not available (requires GPU CC >= 7.0)",
124+
)
125+
new_config = {"engine": "xtts", "voice": "marina"}
126+
logger.info("🎤 Голос изменён на: Марина (XTTS)")
127+
128+
elif voice_id == "marina_openvoice":
129+
if not container.openvoice_service:
130+
raise HTTPException(status_code=503, detail="OpenVoice service not available")
131+
new_config = {"engine": "openvoice", "voice": "marina_openvoice"}
132+
logger.info("🎤 Голос изменён на: Марина (OpenVoice)")
133+
134+
elif voice_id in ["dmitri", "irina"]:
135+
if not container.piper_service:
136+
raise HTTPException(status_code=503, detail="Piper TTS service not available")
137+
piper_voices = container.piper_service.get_available_voices()
138+
if voice_id not in piper_voices or not piper_voices[voice_id]["available"]:
139+
raise HTTPException(status_code=400, detail=f"Voice model not found: {voice_id}")
140+
new_config = {"engine": "piper", "voice": voice_id}
141+
logger.info(f"🎤 Голос изменён на: {piper_voices[voice_id]['name']} (Piper)")
142+
143+
else:
144+
raise HTTPException(
145+
status_code=400,
146+
detail=f"Unknown voice: {voice_id}. Available: anna, marina, marina_openvoice, dmitri, irina",
147+
)
148+
149+
container.current_voice_config = new_config
150+
return {"status": "ok", **new_config}
151+
152+
153+
@router.post("/admin/voice/test")
154+
async def admin_test_voice(request: VoiceRequest):
155+
"""Тестовый синтез выбранным голосом"""
156+
container = get_container()
157+
voice_id = request.voice.lower()
158+
test_text = "Здравствуйте! Это тестовое сообщение для проверки голоса."
159+
160+
output_path = TEMP_DIR / f"voice_test_{voice_id}_{int(time.time())}.wav"
161+
162+
try:
163+
if voice_id == "anna":
164+
if not container.anna_voice_service:
165+
raise HTTPException(
166+
status_code=503, detail="XTTS (Анна) not available (requires GPU CC >= 7.0)"
167+
)
168+
container.anna_voice_service.synthesize_to_file(
169+
test_text, str(output_path), preset="natural"
170+
)
171+
172+
elif voice_id == "marina":
173+
if not container.voice_service:
174+
raise HTTPException(
175+
status_code=503, detail="XTTS (Марина) not available (requires GPU CC >= 7.0)"
176+
)
177+
container.voice_service.synthesize_to_file(
178+
test_text, str(output_path), preset="natural"
179+
)
180+
181+
elif voice_id == "marina_openvoice":
182+
if not container.openvoice_service:
183+
raise HTTPException(status_code=503, detail="OpenVoice not available")
184+
container.openvoice_service.synthesize_to_file(
185+
test_text, str(output_path), language="ru"
186+
)
187+
188+
elif voice_id in ["dmitri", "irina"]:
189+
if not container.piper_service:
190+
raise HTTPException(status_code=503, detail="Piper not available")
191+
container.piper_service.synthesize_to_file(test_text, str(output_path), voice=voice_id)
192+
193+
else:
194+
raise HTTPException(
195+
status_code=400,
196+
detail=f"Unknown voice: {voice_id}. Available: anna, marina, marina_openvoice, dmitri, irina",
197+
)
198+
199+
return FileResponse(output_path, media_type="audio/wav", filename=f"test_{voice_id}.wav")
200+
201+
except HTTPException:
202+
raise
203+
except Exception as e:
204+
logger.error(f"❌ Ошибка тестового синтеза: {e}")
205+
raise HTTPException(status_code=500, detail=str(e))

0 commit comments

Comments
 (0)