From 10a1d28f77f5a03d1811451c04a6c7c7e2e3f3aa Mon Sep 17 00:00:00 2001 From: Daniil Date: Sat, 4 Apr 2026 00:11:06 +0300 Subject: [PATCH] feat(frontend): add SaluteSpeech engine option to TranscriptionSettingsStep Co-Authored-By: Claude Sonnet 4.6 --- .../TranscriptionSettingsStep.tsx | 329 ++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 src/features/project/TranscriptionSettingsStep/TranscriptionSettingsStep.tsx diff --git a/src/features/project/TranscriptionSettingsStep/TranscriptionSettingsStep.tsx b/src/features/project/TranscriptionSettingsStep/TranscriptionSettingsStep.tsx new file mode 100644 index 0000000..c8e9a40 --- /dev/null +++ b/src/features/project/TranscriptionSettingsStep/TranscriptionSettingsStep.tsx @@ -0,0 +1,329 @@ +"use client" + +import type { ITranscriptionSettingsStepProps } from "./TranscriptionSettingsStep.d" +import type { JSX } from "react" + +import { Info } from "lucide-react" +import { FunctionComponent, useEffect, useState } from "react" +import { Controller, useForm } from "react-hook-form" + +import cs from "classnames" + +import api from "@shared/api" +import { useWizard } from "@shared/context/WizardContext" +import { useAppSelector } from "@shared/hooks/useAppSelector" +import { Button, CircularProgress, Form, Select, SelectItem } from "@shared/ui" + +import { useSubmitTranscription } from "../TranscriptionModal/useSubmitTranscription" +import { buildCancelJobPayload, useCancelJob } from "../useCancelJob" +import styles from "./TranscriptionSettingsStep.module.scss" + +interface ITranscriptionFormData { + engine: "whisper" | "google" | "salutespeech" + language: string + model: string +} + +const ENGINE_OPTIONS = [ + { value: "whisper", label: "Whisper (локальный)" }, + { value: "google", label: "Google Speech" }, + { value: "salutespeech", label: "SaluteSpeech" }, +] + +const LANGUAGE_OPTIONS = [ + { value: "auto", label: "Авто" }, + { value: "ru", label: "Русский" }, + { value: "en", label: "Английский" }, +] + +const WHISPER_MODEL_OPTIONS = [ + { value: "base", label: "Базовая" }, + { value: "small", label: "Малая" }, + { value: "medium", label: "Средняя" }, + { value: "large", label: "Большая" }, +] + +const SALUTE_MODEL_OPTIONS = [ + { value: "general", label: "Общая" }, + { value: "finance", label: "Финансы" }, + { value: "medicine", label: "Медицина" }, +] + +export const TranscriptionSettingsStep: FunctionComponent< + ITranscriptionSettingsStepProps +> = ({ className }): JSX.Element => { + const { + projectId, + primaryFileKey, + activeJobId, + activeJobType, + setActiveJob, + startProcessingJob, + goBack, + } = useWizard() + + const isProcessing = + !!activeJobId && activeJobType === "TRANSCRIPTION_GENERATE" + + const [submitError, setSubmitError] = useState(null) + const { mutate: cancelJob, isPending: isCancelling } = useCancelJob() + + const { control, handleSubmit, watch, setValue } = + useForm({ + defaultValues: { + engine: "whisper", + language: "auto", + model: "base", + }, + }) + + const engine = watch("engine") + + useEffect(() => { + if (engine === "salutespeech") { + setValue("model", "general") + } else if (engine === "whisper") { + setValue("model", "base") + } + }, [engine, setValue]) + + const { mutate, isPending } = useSubmitTranscription({ + onSuccess: (data) => { + if (data?.job_id) { + startProcessingJob( + data.job_id, + "TRANSCRIPTION_GENERATE", + "transcription-processing", + "transcription-settings", + ) + } + }, + onError: (error) => { + console.error("Transcription submit failed:", error) + setSubmitError("Не удалось запустить транскрипцию") + }, + }) + + const onSubmit = (data: ITranscriptionFormData): void => { + if (!primaryFileKey) return + setSubmitError(null) + + mutate({ + body: { + file_key: primaryFileKey, + project_id: projectId, + engine: data.engine, + language: data.language === "auto" ? undefined : data.language, + model: data.model, + }, + }) + } + + /* ---- Processing state (inline) ---- */ + + const notification = useAppSelector((state) => + activeJobId + ? state.notifications.items.find((n) => n.job_id === activeJobId) + : null, + ) + + const { data: taskStatus } = api.useQuery( + "get", + "/api/tasks/status/{job_id}/", + { params: { path: { job_id: activeJobId ?? "" } } }, + { enabled: isProcessing, refetchInterval: 2000 }, + ) + + const progressPct = notification?.progress_pct ?? 0 + const statusMessage = + notification?.message ?? "Подождите, идёт транскрипция..." + const isFailed = + notification?.status === "FAILED" || taskStatus?.status === "FAILED" + + const handleCancel = () => { + if (isFailed) { + setActiveJob(null) + return + } + + if (!activeJobId || isCancelling) return + + cancelJob(buildCancelJobPayload(activeJobId), { + onSuccess: () => { + setActiveJob(null) + }, + }) + } + + if (isProcessing) { + return ( +
+
+
+ +
+ + {Math.round(progressPct)}% + + + {isFailed ? "ОШИБКА" : "ТРАНСКРИБАЦИЯ"} + +
+
+ +

+ {isFailed + ? (notification?.message ?? "Произошла ошибка при транскрипции") + : statusMessage} +

+ +
+ + + Обработка выполняется на сервере. Вы можете покинуть страницу — + прогресс сохранится. + +
+ + +
+
+ ) + } + + /* ---- Settings form ---- */ + + return ( +
+
+
+

Параметры транскрипции

+

+ Настройте параметры для генерации субтитров +

+
+ +
+
+
+
Движок
+ ( + + )} + /> +
+ +
+
Язык
+ ( + + )} + /> +
+ + {(engine === "whisper" || engine === "salutespeech") && ( +
+
Модель
+ ( + + )} + /> +
+ )} +
+ + {submitError &&

{submitError}

} + +
+ + +
+
+
+
+ ) +}