chore: something changed, commit before reorg
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user