lct-08: каркас фронта, типизированный сокет, захват микрофона
Направление канала зашито в тип: у наблюдателя Out = never, отправить нечего и нельзя; у преподавателя нет входящих. Разделение прав архитектурное и на фронте тоже. Пока соединения нет, события не копятся: истина на сервере, после переподключения придёт актуальное состояние. Ресемплинг 48 → 16 кГц усредняет по окну выходного сэмпла: без фильтра всё выше 8 кГц наложилось бы на речь и испортило распознавание. Проверено числами: секунда звука — ровно 50 кадров по 320 сэмплов из 48 и 44.1 кГц, помеха 15 кГц подавлена до 7.8%. Эхоподавление и автоусиление браузера выключены: гарнитура эхо не даёт, а обработка портит речь для STT. Бэкенд принимает бинарные кадры по тому же сокету, что и события; кадр не того размера отбрасывается с одним предупреждением, а не 50 в секунду. Логирование приложения не было настроено: uvicorn настраивает только свои логгеры, и весь INFO модулей app.* молча терялся — нашлось, когда в логе не оказалось строки о принятых кадрах. make lesson запускает занятие и печатает ссылки на экраны — пульта преподавателя ещё нет, а открыть АРМ курсанта без занятия не с чем. Путь из браузера с живым микрофоном не проверен: нужен человек, шаги записаны в карточке.
This commit is contained in:
parent
f01f2dab76
commit
f24031b5b2
21 changed files with 594 additions and 4 deletions
66
frontend/src/shared/api/http.ts
Normal file
66
frontend/src/shared/api/http.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// HTTP-клиент и хуки. Типы ответов HTTP в generated.ts пока не попадают —
|
||||
// там только события WS; здесь описаны руками по docs/arch/CONTRACT.md#http-api.
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { Level, SessionMode } from "@/shared/types/generated";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly code: string,
|
||||
) {
|
||||
super(`${status} ${code}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: { "Content-Type": "application/json", ...init?.headers },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
throw new ApiError(response.status, body.detail ?? response.statusText);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export interface Health {
|
||||
status: string;
|
||||
models_ready: boolean;
|
||||
embeddings_ready: boolean;
|
||||
scenarios_loaded: number;
|
||||
offline: boolean;
|
||||
}
|
||||
|
||||
export interface ScenarioSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
level: Level;
|
||||
topics: string[];
|
||||
modes: SessionMode[];
|
||||
dds: string | null;
|
||||
}
|
||||
|
||||
export interface SessionInfo {
|
||||
session_id: string;
|
||||
scenario_id: string;
|
||||
mode: SessionMode;
|
||||
attempt: number;
|
||||
trainee_id: string | null;
|
||||
group_id: string | null;
|
||||
}
|
||||
|
||||
export const useHealth = () =>
|
||||
useQuery({ queryKey: ["health"], queryFn: () => request<Health>("/api/health"), refetchInterval: 5_000 });
|
||||
|
||||
export const useScenarios = () =>
|
||||
useQuery({ queryKey: ["scenarios"], queryFn: () => request<ScenarioSummary[]>("/api/scenarios") });
|
||||
|
||||
export const useCreateSession = () =>
|
||||
useMutation({
|
||||
mutationFn: (body: { scenario_id: string; mode: SessionMode; trainee?: string; group?: string }) =>
|
||||
request<SessionInfo>("/api/sessions", { method: "POST", body: JSON.stringify(body) }),
|
||||
});
|
||||
Loading…
Reference in a new issue