chore: something changed, commit before reorg

This commit is contained in:
Daniil
2026-04-27 23:28:28 +03:00
parent 46f34bdcac
commit 20928e9a60
16 changed files with 1967 additions and 1262 deletions
@@ -43,57 +43,16 @@ export const CaptionResultStep: FunctionComponent<ICaptionResultStepProps> = ({
className,
}): JSX.Element => {
const {
projectId,
captionedVideoFileId,
captionedVideoPath,
goToStep,
markStepCompleted,
setCaptionedVideoFileId,
setCaptionedVideoPath,
reopenCaptionConfig,
} = useWizard()
// Recovery: if wizard state lost the file data, look up the latest caption job
const needsRecovery = !captionedVideoFileId && !captionedVideoPath
const { data: jobs } = api.useQuery(
"get",
"/api/jobs/jobs/",
{},
{ enabled: needsRecovery },
)
const recoveredJob = useMemo(() => {
if (!needsRecovery || !jobs) return null
return jobs.find(
(j) =>
j.project_id === projectId &&
j.job_type === "CAPTIONS_GENERATE" &&
j.status === "DONE" &&
j.output_data?.file_id,
)
}, [needsRecovery, jobs, projectId])
const effectiveFileId =
captionedVideoFileId ??
(recoveredJob?.output_data?.file_id as string | undefined) ??
null
const effectivePath =
captionedVideoPath ??
(recoveredJob?.output_data?.output_path as string | undefined) ??
null
// Persist recovered values back to wizard state
if (recoveredJob && !captionedVideoFileId && effectiveFileId) {
setCaptionedVideoFileId(effectiveFileId)
}
if (recoveredJob && !captionedVideoPath && effectivePath) {
setCaptionedVideoPath(effectivePath)
}
const { data: fileInfo, isLoading } = api.useQuery(
"get",
"/api/files/files/{file_id}/resolve/",
{ params: { path: { file_id: effectiveFileId ?? "" } } },
{ enabled: !!effectiveFileId },
{ params: { path: { file_id: captionedVideoFileId ?? "" } } },
{ enabled: !!captionedVideoFileId },
)
const videoUrl = fileInfo?.file_url ?? ""
@@ -107,7 +66,7 @@ export const CaptionResultStep: FunctionComponent<ICaptionResultStepProps> = ({
}
const handleRerender = () => {
goToStep("caption-settings")
void reopenCaptionConfig()
}
const handleFinish = () => {
@@ -210,21 +169,21 @@ export const CaptionResultStep: FunctionComponent<ICaptionResultStepProps> = ({
{/* Footer */}
<div className={styles.footer}>
<Button variant="ghost" onClick={handleRerender}>
<RefreshCw size={16} />
Перегенерировать
</Button>
<div className={styles.rightActions}>
<Button variant="primary" onClick={handleDownload}>
<Button variant="ghost" onClick={handleRerender}>
<RefreshCw size={16} />
Перегенерировать
</Button>
<div className={styles.rightActions}>
<Button variant="primary" onClick={handleDownload}>
<Download size={16} />
Скачать
</Button>
<Button variant="outline" onClick={handleFinish}>
<Check size={16} />
Завершить
</Button>
<Button variant="outline" onClick={handleFinish}>
<Check size={16} />
Завершить
</Button>
</div>
</div>
</div>
</div>
)
}
@@ -6,142 +6,58 @@ import type { JSX } from "react"
import {
FunctionComponent,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import cs from "classnames"
import api from "@shared/api"
import { useWizard } from "@shared/context/WizardContext"
import { Button } from "@shared/ui"
import { PresetGrid } from "./PresetGrid"
import { StyleEditor } from "./StyleEditor"
import { useSubmitCaptionGenerate } from "./useSubmitCaptionGenerate"
import styles from "./CaptionSettingsStep.module.scss"
type CaptionPresetRead = components["schemas"]["CaptionPresetRead"]
const ERROR_SUBMIT = "Не удалось запустить генерацию субтитров"
const ERROR_MISSING_DATA =
"Для генерации субтитров необходимы видеофайл и транскрипция. Пройдите предыдущие шаги."
const TRANSCRIPTION_ARTIFACT_TYPE = "TRANSCRIPTION_JSON"
export const CaptionSettingsStep: FunctionComponent<
ICaptionSettingsStepProps
> = ({ className }): JSX.Element => {
const {
projectId,
primaryFileKey,
transcriptionArtifactId: contextArtifactId,
captionPresetId,
setCaptionPresetId,
setTranscriptionArtifactId,
startProcessingJob,
selectCaptionPreset,
startCaptionRender,
goBack,
} = useWizard()
const { data: artifacts, isLoading: isArtifactsLoading } = api.useQuery(
"get",
"/api/media/artifacts/",
{},
{ enabled: !contextArtifactId },
)
const transcriptionArtifactId = useMemo(() => {
if (contextArtifactId) return contextArtifactId
if (!artifacts) return null
const match = artifacts.find(
(artifact) =>
artifact.project_id === projectId &&
artifact.artifact_type === TRANSCRIPTION_ARTIFACT_TYPE &&
!artifact.is_deleted,
)
return match?.id ?? null
}, [artifacts, contextArtifactId, projectId])
useEffect(() => {
if (
!transcriptionArtifactId ||
transcriptionArtifactId === contextArtifactId
) {
return
}
setTranscriptionArtifactId(transcriptionArtifactId)
}, [
contextArtifactId,
setTranscriptionArtifactId,
transcriptionArtifactId,
])
const { data: transcriptionEntry, isLoading: isTranscriptionLoading } =
api.useQuery(
"get",
"/api/transcribe/transcriptions/by-artifact/{artifact_id}/",
{
params: {
path: { artifact_id: transcriptionArtifactId ?? "" },
},
},
{ enabled: !!transcriptionArtifactId },
)
const [activeTab, setActiveTab] = useState<"select" | "editor">("select")
const [editingPreset, setEditingPreset] = useState<CaptionPresetRead | null>(
null,
)
const [submitError, setSubmitError] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const submitLockRef = useRef(false)
const isResolvingSourceData = isArtifactsLoading || isTranscriptionLoading
const { mutate, isPending } = useSubmitCaptionGenerate({
onSuccess: (data) => {
if (!data?.job_id) {
submitLockRef.current = false
return
}
if (data?.job_id) {
startProcessingJob(
data.job_id,
"CAPTIONS_GENERATE",
"caption-processing",
"caption-settings",
)
}
},
onError: () => {
submitLockRef.current = false
setSubmitError(ERROR_SUBMIT)
},
})
const handleGenerate = () => {
if (submitLockRef.current || isPending) return
const transcriptionId = transcriptionEntry?.id
if (!primaryFileKey || !transcriptionId) {
setSubmitError(ERROR_MISSING_DATA)
return
}
const handleGenerate = async () => {
if (submitLockRef.current || isSubmitting) return
if (!captionPresetId) return
submitLockRef.current = true
setSubmitError(null)
mutate({
body: {
video_s3_path: primaryFileKey,
folder: "output_files",
transcription_id: transcriptionId,
project_id: projectId,
preset_id: captionPresetId,
},
})
setIsSubmitting(true)
try {
await startCaptionRender()
submitLockRef.current = false
} catch {
submitLockRef.current = false
setSubmitError(ERROR_SUBMIT)
} finally {
setIsSubmitting(false)
}
}
const handleEdit = (preset: CaptionPresetRead) => {
@@ -155,10 +71,14 @@ export const CaptionSettingsStep: FunctionComponent<
}
const handleSaved = (presetId: string) => {
setCaptionPresetId(presetId)
void selectCaptionPreset(presetId)
setActiveTab("select")
}
const handleSelectPreset = (presetId: string | null) => {
void selectCaptionPreset(presetId)
}
if (activeTab === "editor") {
return (
<div
@@ -187,7 +107,7 @@ export const CaptionSettingsStep: FunctionComponent<
<div className={styles.scrollArea}>
<PresetGrid
selectedPresetId={captionPresetId}
onSelect={setCaptionPresetId}
onSelect={handleSelectPreset}
onEdit={handleEdit}
onCreateNew={handleCreateNew}
/>
@@ -202,11 +122,9 @@ export const CaptionSettingsStep: FunctionComponent<
<Button
variant="primary"
onClick={handleGenerate}
disabled={
!captionPresetId || isPending || isResolvingSourceData
}
disabled={!captionPresetId || isSubmitting}
>
{isPending ? "Запуск..." : "Генерировать"}
{isSubmitting ? "Запуск..." : "Генерировать"}
</Button>
</div>
</div>
@@ -23,12 +23,11 @@ import {
import WaveSurfer from "wavesurfer.js"
import api from "@shared/api"
import { useProjectWorkspaceQuery } from "@shared/api/projectWorkflow"
import { useWizard } from "@shared/context/WizardContext"
import { useSegmentResize } from "@shared/hooks/useSegmentResize"
import { Button } from "@shared/ui"
import { useSubmitSilenceApply } from "../SilenceResultModal/useSubmitSilenceApply"
import styles from "./FragmentsStep.module.scss"
const MIN_REGION_MS = 100
@@ -71,14 +70,12 @@ export const FragmentsStep: FunctionComponent<IFragmentsStepProps> = ({
}): JSX.Element => {
const {
projectId,
silenceJobId,
primaryFileId,
primaryFileKey,
startProcessingJob,
startSilenceApply,
skipSilenceApply,
goBack,
markStepCompleted,
goToStep,
} = useWizard()
const { data: workspace } = useProjectWorkspaceQuery(projectId)
const [cutRegions, setCutRegions] = useState<CutRegion[]>([])
const [pixelsPerSecond, setPixelsPerSecond] = useState(DEFAULT_PPS)
@@ -94,16 +91,7 @@ export const FragmentsStep: FunctionComponent<IFragmentsStepProps> = ({
const waveformRef = useRef<HTMLDivElement>(null)
const wsRef = useRef<WaveSurfer | null>(null)
/* ---- Data loading ---- */
const { data: taskStatus } = api.useQuery(
"get",
"/api/tasks/status/{job_id}/",
{ params: { path: { job_id: silenceJobId ?? "" } } },
{ enabled: !!silenceJobId },
)
const outputData = taskStatus?.output_data as Record<string, unknown> | null
const fileKey = primaryFileKey ?? ((outputData?.file_key as string) ?? "")
const silenceState = workspace?.silence
const { data: fileInfo } = api.useQuery(
"get",
@@ -116,11 +104,12 @@ export const FragmentsStep: FunctionComponent<IFragmentsStepProps> = ({
/* ---- Initialize cut regions from detection results ---- */
useEffect(() => {
if (!outputData) return
const segments = outputData.silent_segments as
| { start_ms: number; end_ms: number }[]
| undefined
const dur = outputData.duration_ms as number | undefined
if (!silenceState) return
const segments =
silenceState.reviewed_cuts.length > 0
? silenceState.reviewed_cuts
: silenceState.detected_segments
const dur = silenceState.duration_ms
if (segments && dur) {
setDurationMs(dur)
@@ -132,8 +121,7 @@ export const FragmentsStep: FunctionComponent<IFragmentsStepProps> = ({
})),
)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [outputData])
}, [silenceState])
/* ---- Timeline calculations ---- */
const totalWidth = Math.max(1, (durationMs / 1000) * pixelsPerSecond)
@@ -599,49 +587,30 @@ export const FragmentsStep: FunctionComponent<IFragmentsStepProps> = ({
}
}, [drawRuler, drawFrames])
/* ---- Apply ---- */
const { mutate: applyMutate, isPending: isApplying } =
useSubmitSilenceApply({
onSuccess: (data) => {
const result = data as { job_id?: string }
if (result?.job_id) {
startProcessingJob(
result.job_id,
"SILENCE_APPLY",
"silence-apply-processing",
"fragments",
)
}
},
onError: (error) => {
console.error("Silence apply failed:", error)
},
})
const [isApplying, setIsApplying] = useState(false)
const handleApply = () => {
const handleApply = async () => {
if (cutRegions.length === 0) {
markStepCompleted("fragments")
goToStep("transcription-settings")
setIsApplying(true)
try {
await skipSilenceApply()
} finally {
setIsApplying(false)
}
return
}
if (!fileKey) return
const fileName = fileKey.split("/").pop() ?? "video.mp4"
const outputName = `Без тишины ${fileName}`
;(applyMutate as (args: { body: Record<string, unknown> }) => void)({
body: {
file_key: fileKey,
out_folder: "",
project_id: projectId,
output_name: outputName,
cuts: cutRegions.map((r) => ({
setIsApplying(true)
try {
await startSilenceApply(
cutRegions.map((r) => ({
start_ms: Math.round(r.startMs),
end_ms: Math.round(r.endMs),
})),
},
})
)
} finally {
setIsApplying(false)
}
}
return (
@@ -23,6 +23,7 @@ import {
import WaveSurfer from "wavesurfer.js"
import api from "@shared/api"
import { useProjectWorkspaceQuery } from "@shared/api/projectWorkflow"
import { useSegmentResize } from "@shared/hooks/useSegmentResize"
import { Button, Modal } from "@shared/ui"
@@ -70,6 +71,7 @@ export const SilenceResultModal: FunctionComponent<ISilenceResultModalProps> = (
projectId,
jobId,
}): JSX.Element => {
const { data: workspace } = useProjectWorkspaceQuery(projectId)
const [cutRegions, setCutRegions] = useState<CutRegion[]>([])
const [pixelsPerSecond, setPixelsPerSecond] = useState(DEFAULT_PPS)
const [durationMs, setDurationMs] = useState(0)
@@ -95,16 +97,7 @@ export const SilenceResultModal: FunctionComponent<ISilenceResultModalProps> = (
const outputData = taskStatus?.output_data as Record<string, unknown> | null
const fileKey = (outputData?.file_key as string) ?? ""
const { data: project } = api.useQuery(
"get",
"/api/projects/{project_id}/",
{ params: { path: { project_id: projectId } } },
{ enabled: open },
)
const primaryFileId =
(project?.workspace_state as { wizard?: { primary_file_id?: string | null } } | null)
?.wizard?.primary_file_id ?? null
const primaryFileId = workspace?.source_file_id ?? null
const { data: fileInfo } = api.useQuery(
"get",
@@ -4,57 +4,32 @@ import type { ISilenceSettingsStepProps } from "./SilenceSettingsStep.d"
import type { JSX } from "react"
import cs from "classnames"
import { FunctionComponent, useCallback } from "react"
import { FunctionComponent, useCallback, useEffect, useState } from "react"
import { useWizard } from "@shared/context/WizardContext"
import { Button, Slider } from "@shared/ui"
import { useSubmitSilenceDetect } from "../SilenceSettingsModal/useSubmitSilenceDetect"
import styles from "./SilenceSettingsStep.module.scss"
export const SilenceSettingsStep: FunctionComponent<
ISilenceSettingsStepProps
> = ({ className }): JSX.Element => {
const {
projectId,
primaryFileKey,
silenceSettings,
setSilenceSettings,
startProcessingJob,
startSilenceDetect,
goBack,
} = useWizard()
const [localSettings, setLocalSettings] = useState(silenceSettings)
const { mutate, isPending } = useSubmitSilenceDetect({
onSuccess: (data) => {
const result = data as { job_id?: string }
if (result?.job_id) {
startProcessingJob(
result.job_id,
"SILENCE_DETECT",
"processing",
"silence-settings",
)
}
},
onError: (error) => {
console.error("Silence detect submit failed:", error)
},
})
useEffect(() => {
setLocalSettings(silenceSettings)
}, [silenceSettings])
const handleSubmit = useCallback(() => {
if (!primaryFileKey) return
;(mutate as (args: { body: Record<string, unknown> }) => void)({
body: {
file_key: primaryFileKey,
project_id: projectId,
min_silence_duration_ms: silenceSettings.min_silence_duration_ms,
silence_threshold_db: silenceSettings.silence_threshold_db,
padding_ms: silenceSettings.padding_ms,
},
})
}, [mutate, primaryFileKey, projectId, silenceSettings])
void startSilenceDetect(localSettings)
}, [localSettings, primaryFileKey, startSilenceDetect])
return (
<div
@@ -73,15 +48,15 @@ export const SilenceSettingsStep: FunctionComponent<
<div className={styles.fields}>
<Slider
label="Мин. длительность тишины"
value={silenceSettings.min_silence_duration_ms}
value={localSettings.min_silence_duration_ms}
min={100}
max={2000}
step={50}
unit="мс"
helpText="Минимальная длительность тихого участка для обнаружения"
onChange={(v) =>
setSilenceSettings({
...silenceSettings,
setLocalSettings({
...localSettings,
min_silence_duration_ms: v,
})
}
@@ -89,15 +64,15 @@ export const SilenceSettingsStep: FunctionComponent<
<Slider
label="Порог тишины"
value={silenceSettings.silence_threshold_db}
value={localSettings.silence_threshold_db}
min={6}
max={40}
step={2}
unit="дБ"
helpText="Уровень громкости ниже которого звук считается тишиной"
onChange={(v) =>
setSilenceSettings({
...silenceSettings,
setLocalSettings({
...localSettings,
silence_threshold_db: v,
})
}
@@ -105,15 +80,15 @@ export const SilenceSettingsStep: FunctionComponent<
<Slider
label="Отступ"
value={silenceSettings.padding_ms}
value={localSettings.padding_ms}
min={0}
max={500}
step={25}
unit="мс"
helpText="Дополнительный отступ по краям тихих участков"
onChange={(v) =>
setSilenceSettings({
...silenceSettings,
setLocalSettings({
...localSettings,
padding_ms: v,
})
}
@@ -123,15 +98,15 @@ export const SilenceSettingsStep: FunctionComponent<
{/* Footer */}
<div className={styles.footer}>
<Button variant="outline" onClick={goBack} disabled={isPending}>
<Button variant="outline" onClick={goBack}>
Назад
</Button>
<Button
variant="primary"
onClick={handleSubmit}
disabled={isPending || !primaryFileKey}
disabled={!primaryFileKey}
>
{isPending ? "Запуск..." : "Далее"}
Далее
</Button>
</div>
</div>
@@ -15,13 +15,10 @@ import {
import "@vidstack/react/player/styles/default/theme.css"
import "@vidstack/react/player/styles/default/layouts/video.css"
import cs from "classnames"
import { FunctionComponent, useEffect, useMemo, useRef } from "react"
import { FunctionComponent, useEffect, useRef } from "react"
import api from "@shared/api"
import {
StaticWorkspaceProvider,
useWorkspaceFiles,
} from "@shared/context/WorkspaceContext"
import { useWorkspaceFiles } from "@shared/context/WorkspaceContext"
import { useWizard } from "@shared/context/WizardContext"
import { Button } from "@shared/ui"
import { TranscriptionEditor } from "@features/project"
@@ -29,8 +26,6 @@ import { TimelinePanel } from "@widgets/TimelinePanel"
import styles from "./SubtitleRevisionStep.module.scss"
const TRANSCRIPTION_ARTIFACT_TYPE = "TRANSCRIPTION_JSON"
/**
* Auto-initializes WorkspaceContext with the video file
* and transcription artifact so TimelinePanel and
@@ -87,51 +82,15 @@ const SubtitleRevisionContent: FunctionComponent<{
projectId,
videoUrl,
primaryFileKey,
transcriptionArtifactId: contextArtifactId,
setTranscriptionArtifactId,
transcriptionArtifactId,
goBack,
goToStep,
markStepCompleted,
markTranscriptionReviewed,
} = useWizard()
const { data: artifacts, isLoading: isArtifactsLoading } = api.useQuery(
"get",
"/api/media/artifacts/",
{},
{ enabled: !contextArtifactId },
)
const transcriptionArtifactId = useMemo(() => {
if (contextArtifactId) return contextArtifactId
if (!artifacts) return null
const match = artifacts.find(
(a) =>
a.project_id === projectId &&
a.artifact_type === TRANSCRIPTION_ARTIFACT_TYPE &&
!a.is_deleted,
)
return match?.id ?? null
}, [contextArtifactId, artifacts, projectId])
const isArtifactResolving = !contextArtifactId && isArtifactsLoading
const isArtifactResolving = false
const isTranscriptionReady = Boolean(transcriptionArtifactId)
const isTranscriptionUnavailable =
!isTranscriptionReady && !isArtifactResolving
useEffect(() => {
if (
!transcriptionArtifactId ||
transcriptionArtifactId === contextArtifactId
) {
return
}
setTranscriptionArtifactId(transcriptionArtifactId)
}, [
contextArtifactId,
setTranscriptionArtifactId,
transcriptionArtifactId,
])
// Auto-trigger frame extraction so video frames appear in timeline
const frameExtractMutation = api.useMutation(
"post",
@@ -154,9 +113,7 @@ const SubtitleRevisionContent: FunctionComponent<{
const handleFinish = () => {
if (!isTranscriptionReady) return
markStepCompleted("subtitle-revision")
goToStep("caption-settings")
void markTranscriptionReviewed()
}
return (
@@ -280,9 +237,5 @@ const SubtitleRevisionContent: FunctionComponent<{
export const SubtitleRevisionStep: FunctionComponent<
ISubtitleRevisionStepProps
> = ({ className }): JSX.Element => {
return (
<StaticWorkspaceProvider>
<SubtitleRevisionContent className={className} />
</StaticWorkspaceProvider>
)
return <SubtitleRevisionContent className={className} />
}
@@ -13,8 +13,6 @@ 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"
@@ -53,12 +51,11 @@ export const TranscriptionSettingsStep: FunctionComponent<
ITranscriptionSettingsStepProps
> = ({ className }): JSX.Element => {
const {
projectId,
primaryFileKey,
activeJobId,
activeJobType,
setActiveJob,
startProcessingJob,
startTranscription,
goToStep,
} = useWizard()
@@ -66,6 +63,7 @@ export const TranscriptionSettingsStep: FunctionComponent<
!!activeJobId && activeJobType === "TRANSCRIPTION_GENERATE"
const [submitError, setSubmitError] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const { mutate: cancelJob, isPending: isCancelling } = useCancelJob()
const { control, handleSubmit, watch, setValue } =
@@ -87,36 +85,23 @@ export const TranscriptionSettingsStep: FunctionComponent<
}
}, [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 => {
const onSubmit = async (data: ITranscriptionFormData): Promise<void> => {
if (!primaryFileKey) return
setSubmitError(null)
setIsSubmitting(true)
mutate({
body: {
file_key: primaryFileKey,
project_id: projectId,
try {
await startTranscription({
engine: data.engine,
language: data.language === "auto" ? undefined : data.language,
model: data.model,
},
})
})
} catch (error) {
console.error("Transcription submit failed:", error)
setSubmitError("Не удалось запустить транскрипцию")
} finally {
setIsSubmitting(false)
}
}
/* ---- Processing state (inline) ---- */
@@ -309,7 +294,7 @@ export const TranscriptionSettingsStep: FunctionComponent<
<Button
type="button"
variant="outline"
disabled={isPending}
disabled={isSubmitting}
onClick={() => goToStep("fragments")}
>
Назад
@@ -317,9 +302,9 @@ export const TranscriptionSettingsStep: FunctionComponent<
<Button
type="submit"
variant="primary"
disabled={isPending || !primaryFileKey}
disabled={isSubmitting || !primaryFileKey}
>
{isPending ? "Запуск..." : "Сгенерировать субтитры"}
{isSubmitting ? "Запуск..." : "Сгенерировать субтитры"}
</Button>
</div>
</Form>
@@ -20,7 +20,7 @@ const ERROR_UPLOAD_FAILED = "Не удалось загрузить файл"
export const UploadStep: FunctionComponent<IUploadStepProps> = ({
className,
}): JSX.Element => {
const { projectId, setFileKey, markStepCompleted, goNext } = useWizard()
const { projectId, setFileKey } = useWizard()
const [isDragging, setIsDragging] = useState(false)
const [isUploading, setIsUploading] = useState(false)
const [progress, setProgress] = useState(0)
@@ -39,16 +39,18 @@ export const UploadStep: FunctionComponent<IUploadStepProps> = ({
`projects/${projectId}`,
setProgress,
)
setFileKey(result.file_path, result.file_id, result.filename ?? null)
markStepCompleted("upload")
goNext()
await setFileKey(
result.file_path,
result.file_id,
result.filename ?? null,
)
} catch {
setError(ERROR_UPLOAD_FAILED)
} finally {
setIsUploading(false)
}
},
[projectId, setFileKey, markStepCompleted, goNext],
[projectId, setFileKey],
)
const handleFileChange = useCallback(
+11 -57
View File
@@ -32,7 +32,7 @@ import {
import cs from "classnames"
import api, { fetchClient } from "@shared/api"
import api from "@shared/api"
import { useWizard } from "@shared/context/WizardContext"
import { useTaskProgressState } from "@shared/hooks/useTaskProgressState"
import { Badge, Button, CircularProgress } from "@shared/ui"
@@ -55,19 +55,16 @@ export const VerifyStep: FunctionComponent<IVerifyStepProps> = ({
className,
}): JSX.Element => {
const {
projectId,
primaryFileKey,
videoUrl,
originalFileName,
activeJobId,
activeJobType,
goBack,
goNext,
goToStep,
markStepCompleted,
confirmVerify,
setFileKey,
setActiveJob,
startProcessingJob,
startMediaConvert,
} = useWizard()
const [convertError, setConvertError] = useState<string | null>(null)
@@ -113,27 +110,11 @@ export const VerifyStep: FunctionComponent<IVerifyStepProps> = ({
/* ---- Conversion logic ---- */
const convertMutation = api.useMutation("post", "/api/tasks/media-convert/", {
onSuccess: (data) => {
startProcessingJob(data.job_id, "MEDIA_CONVERT", "verify")
setConvertError(null)
},
onError: () => {
setConvertError(ERROR_CONVERT_FAILED)
},
})
const handleConvert = useCallback(() => {
if (!primaryFileKey) return
convertMutation.mutate({
body: {
file_key: primaryFileKey,
out_folder: `projects/${projectId}`,
output_format: "mp4",
project_id: projectId,
},
void startMediaConvert().catch(() => {
setConvertError(ERROR_CONVERT_FAILED)
})
}, [convertMutation, primaryFileKey, projectId])
}, [startMediaConvert])
const {
progressPct: convertProgressPct,
@@ -149,47 +130,20 @@ export const VerifyStep: FunctionComponent<IVerifyStepProps> = ({
useEffect(() => {
if (!convertJobId || convertStatus !== "converting") return
if (convertTaskStatus === "DONE") {
fetchConvertedFileFromJob(convertJobId)
}
if (convertTaskStatus === "FAILED") {
setActiveJob(null)
setConvertError(convertErrorMessage ?? "Ошибка конвертации")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [convertErrorMessage, convertJobId, convertStatus, convertTaskStatus])
const fetchConvertedFileFromJob = useCallback(
async (jobId: string) => {
const { data: taskStatus } = await fetchClient.GET(
"/api/tasks/status/{job_id}/",
{ params: { path: { job_id: jobId } } },
)
const outputData = taskStatus?.output_data as {
file_id?: string
file_path?: string
} | null
if (outputData?.file_id && outputData?.file_path) {
const convertedName = outputData.file_path.split("/").pop() ?? null
setFileKey(outputData.file_path, outputData.file_id, convertedName)
setActiveJob(null)
}
},
[setFileKey, setActiveJob],
)
}, [convertErrorMessage, convertJobId, convertStatus, convertTaskStatus, setActiveJob])
/* ---- Handlers ---- */
const handleReplace = () => {
setFileKey(null, null, null)
goToStep("upload")
void setFileKey(null, null, null)
}
const handleNext = () => {
markStepCompleted("verify")
goNext()
void confirmVerify()
}
/* ---- Converting view ---- */
@@ -266,7 +220,7 @@ export const VerifyStep: FunctionComponent<IVerifyStepProps> = ({
variant="primary"
size="sm"
onClick={handleConvert}
disabled={convertMutation.isPending}
disabled={!primaryFileKey}
>
Конвертировать в MP4
</Button>
@@ -357,7 +311,7 @@ export const VerifyStep: FunctionComponent<IVerifyStepProps> = ({
variant="primary"
size="sm"
onClick={handleConvert}
disabled={convertMutation.isPending}
disabled={!primaryFileKey}
>
Конвертировать в MP4
</Button>