feat(frontend): add SaluteSpeech engine option to TranscriptionModal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniil
2026-04-04 00:11:00 +03:00
parent 305e72725c
commit cf8ded79d7
2 changed files with 605 additions and 18 deletions
@@ -14,7 +14,7 @@ import styles from "./TranscriptionModal.module.scss"
interface ITranscriptionFormData { interface ITranscriptionFormData {
file_key: string file_key: string
engine: "whisper" | "google" engine: "whisper" | "google" | "salutespeech"
language: string language: string
model: string model: string
} }
@@ -22,25 +22,32 @@ interface ITranscriptionFormData {
const ENGINE_OPTIONS = [ const ENGINE_OPTIONS = [
{ value: "whisper", label: "Whisper (локальный)" }, { value: "whisper", label: "Whisper (локальный)" },
{ value: "google", label: "Google Speech" }, { value: "google", label: "Google Speech" },
{ value: "salutespeech", label: "SaluteSpeech" },
] ]
const LANGUAGE_OPTIONS = [ const LANGUAGE_OPTIONS = [
{ value: "auto", label: "Авто" }, { value: "auto", label: "Авто" },
{ value: "ru", label: "Русский" }, { value: "ru", label: "Русский" },
{ value: "en", label: "English" }, { value: "en", label: "Английский" },
] ]
const MODEL_OPTIONS = [ const WHISPER_MODEL_OPTIONS = [
{ value: "base", label: "Base" }, { value: "base", label: "Базовая" },
{ value: "small", label: "Small" }, { value: "small", label: "Малая" },
{ value: "medium", label: "Medium" }, { value: "medium", label: "Средняя" },
{ value: "large", label: "Large" }, { value: "large", label: "Большая" },
]
const SALUTE_MODEL_OPTIONS = [
{ value: "general", label: "Общая" },
{ value: "finance", label: "Финансы" },
{ value: "medicine", label: "Медицина" },
] ]
export const TranscriptionModal: FunctionComponent< export const TranscriptionModal: FunctionComponent<
ITranscriptionModalProps ITranscriptionModalProps
> = ({ projectId, open, onOpenChange }): JSX.Element => { > = ({ projectId, open, onOpenChange }): JSX.Element => {
const { control, handleSubmit, reset, watch } = const { control, handleSubmit, reset, watch, setValue } =
useForm<ITranscriptionFormData>({ useForm<ITranscriptionFormData>({
defaultValues: { defaultValues: {
file_key: "", file_key: "",
@@ -52,6 +59,14 @@ export const TranscriptionModal: FunctionComponent<
const engine = watch("engine") const engine = watch("engine")
useEffect(() => {
if (engine === "salutespeech") {
setValue("model", "general")
} else if (engine === "whisper") {
setValue("model", "base")
}
}, [engine, setValue])
const { data: files } = api.useQuery("get", "/api/files/files/", { const { data: files } = api.useQuery("get", "/api/files/files/", {
queryKey: ["files", projectId], queryKey: ["files", projectId],
}) })
@@ -159,7 +174,7 @@ export const TranscriptionModal: FunctionComponent<
/> />
</div> </div>
{engine === "whisper" && ( {(engine === "whisper" || engine === "salutespeech") && (
<div className={styles.selectField}> <div className={styles.selectField}>
<div className={styles.selectLabel}>Модель</div> <div className={styles.selectLabel}>Модель</div>
<Controller <Controller
@@ -171,7 +186,10 @@ export const TranscriptionModal: FunctionComponent<
onValueChange={field.onChange} onValueChange={field.onChange}
placeholder="Выберите модель" placeholder="Выберите модель"
> >
{MODEL_OPTIONS.map((opt) => ( {(engine === "whisper"
? WHISPER_MODEL_OPTIONS
: SALUTE_MODEL_OPTIONS
).map((opt) => (
<SelectItem key={opt.value} value={opt.value}> <SelectItem key={opt.value} value={opt.value}>
{opt.label} {opt.label}
</SelectItem> </SelectItem>
+577 -8
View File
@@ -518,6 +518,58 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/api/captions/presets/": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* List Presets
* @description List system presets + user's own presets.
*/
get: operations["list_presets_api_captions_presets__get"];
put?: never;
/**
* Create Preset
* @description Create a user preset.
*/
post: operations["create_preset_api_captions_presets__post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/captions/presets/{preset_id}/": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Get Preset
* @description Get a single preset.
*/
get: operations["get_preset_api_captions_presets__preset_id___get"];
put?: never;
post?: never;
/**
* Delete Preset
* @description Delete a user preset (cannot delete system or others' presets).
*/
delete: operations["delete_preset_api_captions_presets__preset_id___delete"];
options?: never;
head?: never;
/**
* Update Preset
* @description Update a user preset (cannot edit system or others' presets).
*/
patch: operations["update_preset_api_captions_presets__preset_id___patch"];
trace?: never;
};
"/api/jobs/jobs/": { "/api/jobs/jobs/": {
parameters: { parameters: {
query?: never; query?: never;
@@ -632,6 +684,46 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/api/tasks/silence-detect/": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Submit Silence Detect
* @description Submit a background task to detect silent segments in media file.
*/
post: operations["submit_silence_detect_api_tasks_silence_detect__post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/tasks/silence-apply/": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Submit Silence Apply
* @description Submit a background task to apply silence cuts to media file.
*/
post: operations["submit_silence_apply_api_tasks_silence_apply__post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/tasks/media-convert/": { "/api/tasks/media-convert/": {
parameters: { parameters: {
query?: never; query?: never;
@@ -853,7 +945,7 @@ export interface components {
* Artifact Type * Artifact Type
* @enum {string} * @enum {string}
*/ */
artifact_type: "TRANSCRIPTION_JSON" | "SILENCE_REMOVED_VIDEO" | "THUMBNAIL" | "AUDIO_PROXY" | "RENDERED_VIDEO" | "FRAME_SPRITES"; artifact_type: "TRANSCRIPTION_JSON" | "SILENCE_REMOVED_VIDEO" | "CONVERTED_VIDEO" | "THUMBNAIL" | "AUDIO_PROXY" | "RENDERED_VIDEO" | "FRAME_SPRITES";
}; };
/** ArtifactMediaFileRead */ /** ArtifactMediaFileRead */
ArtifactMediaFileRead: { ArtifactMediaFileRead: {
@@ -872,7 +964,7 @@ export interface components {
* Artifact Type * Artifact Type
* @enum {string} * @enum {string}
*/ */
artifact_type: "TRANSCRIPTION_JSON" | "SILENCE_REMOVED_VIDEO" | "THUMBNAIL" | "AUDIO_PROXY" | "RENDERED_VIDEO" | "FRAME_SPRITES"; artifact_type: "TRANSCRIPTION_JSON" | "SILENCE_REMOVED_VIDEO" | "CONVERTED_VIDEO" | "THUMBNAIL" | "AUDIO_PROXY" | "RENDERED_VIDEO" | "FRAME_SPRITES";
/** Is Deleted */ /** Is Deleted */
is_deleted: boolean; is_deleted: boolean;
/** Is Active */ /** Is Active */
@@ -906,6 +998,186 @@ export interface components {
*/ */
folder: string; folder: string;
}; };
/** CaptionAnimationStyle */
CaptionAnimationStyle: {
/**
* Highlight Style
* @default color
* @enum {string}
*/
highlight_style: "color" | "scale" | "underline" | "color_scale";
/**
* Highlight Scale
* @default 1.1
*/
highlight_scale: number;
/**
* Segment Transition
* @default fade
* @enum {string}
*/
segment_transition: "fade" | "slide" | "none";
/**
* Fade Duration Frames
* @default 3
*/
fade_duration_frames: number;
/**
* Animation Speed
* @default 1
*/
animation_speed: number;
};
/** CaptionBackgroundStyle */
CaptionBackgroundStyle: {
/**
* Bg Color
* @default rgba(0,0,0,0.6)
*/
bg_color: string;
/**
* Bg Blur Px
* @default 0
*/
bg_blur_px: number;
/** Bg Glow Color */
bg_glow_color?: string | null;
/**
* Bg Border Radius Px
* @default 15
*/
bg_border_radius_px: number;
/**
* Bg Padding Px
* @default 20
*/
bg_padding_px: number;
};
/** CaptionLayoutStyle */
CaptionLayoutStyle: {
/**
* Vertical Position
* @default bottom
* @enum {string}
*/
vertical_position: "top" | "center" | "bottom";
/**
* Horizontal Alignment
* @default center
* @enum {string}
*/
horizontal_alignment: "left" | "center" | "right";
/**
* Padding Px
* @default 20
*/
padding_px: number;
/**
* Max Width Pct
* @default 90
*/
max_width_pct: number;
/**
* Lines Per Screen
* @default 2
*/
lines_per_screen: number;
};
/** CaptionPresetCreate */
CaptionPresetCreate: {
/** Name */
name: string;
/** Description */
description?: string | null;
style_config: components["schemas"]["CaptionStyleConfig"];
};
/** CaptionPresetRead */
CaptionPresetRead: {
/**
* Id
* Format: uuid
*/
id: string;
/** User Id */
user_id: string | null;
/** Name */
name: string;
/** Description */
description: string | null;
/** Is System */
is_system: boolean;
style_config: components["schemas"]["CaptionStyleConfig"];
/** Preview Url */
preview_url: string | null;
/**
* Created At
* Format: date-time
*/
created_at: string;
/**
* Updated At
* Format: date-time
*/
updated_at: string;
};
/** CaptionPresetUpdate */
CaptionPresetUpdate: {
/** Name */
name?: string | null;
/** Description */
description?: string | null;
style_config?: components["schemas"]["CaptionStyleConfig"] | null;
};
/** CaptionStyleConfig */
CaptionStyleConfig: {
text?: components["schemas"]["CaptionTextStyle"];
layout?: components["schemas"]["CaptionLayoutStyle"];
animation?: components["schemas"]["CaptionAnimationStyle"];
background?: components["schemas"]["CaptionBackgroundStyle"];
};
/** CaptionTextStyle */
CaptionTextStyle: {
/**
* Font Family
* @default Lobster
*/
font_family: string;
/**
* Font Size
* @default 40
*/
font_size: number;
/**
* Font Weight
* @default 400
*/
font_weight: number;
/**
* Text Color
* @default #FFFFFF
*/
text_color: string;
/**
* Highlight Color
* @default #FFCC00
*/
highlight_color: string;
/**
* Text Shadow
* @default 2px 2px 4px rgba(0,0,0,0.5)
*/
text_shadow: string | null;
/**
* Text Stroke Width
* @default 0
*/
text_stroke_width: number;
/**
* Text Stroke Color
* @default #000000
*/
text_stroke_color: string;
};
/** /**
* CaptionsGenerateRequest * CaptionsGenerateRequest
* @description Request to generate captions/subtitles video. * @description Request to generate captions/subtitles video.
@@ -932,6 +1204,18 @@ export interface components {
* @description Associated project ID * @description Associated project ID
*/ */
project_id?: string | null; project_id?: string | null;
/**
* Preset Id
* @description Caption style preset ID (mutually exclusive with style_config)
*/
preset_id?: string | null;
/**
* Style Config
* @description Inline caption style config (overrides preset_id)
*/
style_config?: {
[key: string]: unknown;
} | null;
}; };
/** CaptionsRequest */ /** CaptionsRequest */
CaptionsRequest: { CaptionsRequest: {
@@ -1157,7 +1441,7 @@ export interface components {
* Job Type * Job Type
* @enum {string} * @enum {string}
*/ */
job_type: "MEDIA_PROBE" | "SILENCE_REMOVE" | "MEDIA_CONVERT" | "TRANSCRIPTION_GENERATE" | "CAPTIONS_GENERATE" | "FRAME_EXTRACT"; job_type: "MEDIA_PROBE" | "SILENCE_REMOVE" | "SILENCE_DETECT" | "SILENCE_APPLY" | "MEDIA_CONVERT" | "TRANSCRIPTION_GENERATE" | "CAPTIONS_GENERATE" | "FRAME_EXTRACT";
}; };
/** JobEventCreate */ /** JobEventCreate */
JobEventCreate: { JobEventCreate: {
@@ -1241,7 +1525,7 @@ export interface components {
* Job Type * Job Type
* @enum {string} * @enum {string}
*/ */
job_type: "MEDIA_PROBE" | "SILENCE_REMOVE" | "MEDIA_CONVERT" | "TRANSCRIPTION_GENERATE" | "CAPTIONS_GENERATE" | "FRAME_EXTRACT"; job_type: "MEDIA_PROBE" | "SILENCE_REMOVE" | "SILENCE_DETECT" | "SILENCE_APPLY" | "MEDIA_CONVERT" | "TRANSCRIPTION_GENERATE" | "CAPTIONS_GENERATE" | "FRAME_EXTRACT";
/** Project Pct */ /** Project Pct */
project_pct: number | null; project_pct: number | null;
/** Error Message */ /** Error Message */
@@ -1565,7 +1849,9 @@ export interface components {
*/ */
status: "DRAFT" | "PROCESSING" | "DONE" | "FAILED"; status: "DRAFT" | "PROCESSING" | "DONE" | "FAILED";
/** Workspace State */ /** Workspace State */
workspace_state: Record<string, unknown> | null; workspace_state: {
[key: string]: unknown;
} | null;
/** Is Active */ /** Is Active */
is_active: boolean; is_active: boolean;
/** /**
@@ -1592,7 +1878,9 @@ export interface components {
/** Status */ /** Status */
status?: ("DRAFT" | "PROCESSING" | "DONE" | "FAILED") | null; status?: ("DRAFT" | "PROCESSING" | "DONE" | "FAILED") | null;
/** Workspace State */ /** Workspace State */
workspace_state?: Record<string, unknown> | null; workspace_state?: {
[key: string]: unknown;
} | null;
}; };
/** SegmentNode */ /** SegmentNode */
"SegmentNode-Input": { "SegmentNode-Input": {
@@ -1618,6 +1906,73 @@ export interface components {
/** Lines */ /** Lines */
lines: components["schemas"]["LineNode-Output"][]; lines: components["schemas"]["LineNode-Output"][];
}; };
/**
* SilenceApplyRequest
* @description Request to apply silence cuts to media file.
*/
SilenceApplyRequest: {
/**
* File Key
* @description Storage key of the input file
*/
file_key: string;
/**
* Out Folder
* @description Output folder for processed file
*/
out_folder: string;
/**
* Project Id
* @description Associated project ID
*/
project_id?: string | null;
/**
* Output Name
* @description Display name for the output file
*/
output_name?: string | null;
/**
* Cuts
* @description Cut regions: [{'start_ms': int, 'end_ms': int}, ...]
*/
cuts: {
[key: string]: unknown;
}[];
};
/**
* SilenceDetectRequest
* @description Request to detect silent segments in media file.
*/
SilenceDetectRequest: {
/**
* File Key
* @description Storage key of the input file
*/
file_key: string;
/**
* Project Id
* @description Associated project ID
*/
project_id?: string | null;
/**
* Min Silence Duration Ms
* @description Minimum silence duration in milliseconds
* @default 200
*/
min_silence_duration_ms: number;
/**
* Silence Threshold Db
* @description Silence threshold in decibels
* @default 16
*/
silence_threshold_db: number;
/**
* Padding Ms
* @description Padding around non-silent segments in milliseconds
* @default 100
*/
padding_ms: number;
};
/** /**
* SilenceRemoveRequest * SilenceRemoveRequest
* @description Request to remove silence from media file. * @description Request to remove silence from media file.
@@ -1731,7 +2086,7 @@ export interface components {
* Job Type * Job Type
* @enum {string} * @enum {string}
*/ */
job_type: "MEDIA_PROBE" | "SILENCE_REMOVE" | "MEDIA_CONVERT" | "TRANSCRIPTION_GENERATE" | "CAPTIONS_GENERATE" | "FRAME_EXTRACT"; job_type: "MEDIA_PROBE" | "SILENCE_REMOVE" | "SILENCE_DETECT" | "SILENCE_APPLY" | "MEDIA_CONVERT" | "TRANSCRIPTION_GENERATE" | "CAPTIONS_GENERATE" | "FRAME_EXTRACT";
/** Progress Pct */ /** Progress Pct */
progress_pct?: number | null; progress_pct?: number | null;
/** Current Message */ /** Current Message */
@@ -1839,7 +2194,7 @@ export interface components {
* @default whisper * @default whisper
* @enum {string} * @enum {string}
*/ */
engine: "whisper" | "google"; engine: "whisper" | "google" | "salutespeech";
/** /**
* Language * Language
* @description Language code (e.g., 'en') * @description Language code (e.g., 'en')
@@ -3555,6 +3910,154 @@ export interface operations {
}; };
}; };
}; };
list_presets_api_captions_presets__get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CaptionPresetRead"][];
};
};
};
};
create_preset_api_captions_presets__post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["CaptionPresetCreate"];
};
};
responses: {
/** @description Successful Response */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CaptionPresetRead"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_preset_api_captions_presets__preset_id___get: {
parameters: {
query?: never;
header?: never;
path: {
preset_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CaptionPresetRead"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
delete_preset_api_captions_presets__preset_id___delete: {
parameters: {
query?: never;
header?: never;
path: {
preset_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
update_preset_api_captions_presets__preset_id___patch: {
parameters: {
query?: never;
header?: never;
path: {
preset_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["CaptionPresetUpdate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CaptionPresetRead"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
list_jobs_endpoint_api_jobs_jobs__get: { list_jobs_endpoint_api_jobs_jobs__get: {
parameters: { parameters: {
query?: never; query?: never;
@@ -3917,6 +4420,72 @@ export interface operations {
}; };
}; };
}; };
submit_silence_detect_api_tasks_silence_detect__post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SilenceDetectRequest"];
};
};
responses: {
/** @description Successful Response */
202: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TaskSubmitResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
submit_silence_apply_api_tasks_silence_apply__post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SilenceApplyRequest"];
};
};
responses: {
/** @description Successful Response */
202: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TaskSubmitResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
submit_media_convert_api_tasks_media_convert__post: { submit_media_convert_api_tasks_media_convert__post: {
parameters: { parameters: {
query?: never; query?: never;