chore: something changed, commit before reorg
This commit is contained in:
+477
-8
@@ -200,6 +200,40 @@ export interface paths {
|
||||
patch: operations["patch_project_api_projects__project_id___patch"];
|
||||
trace?: never;
|
||||
};
|
||||
"/api/projects/{project_id}/workspace": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get Project Workspace */
|
||||
get: operations["get_project_workspace_api_projects__project_id__workspace_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/projects/{project_id}/workflow/actions": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Dispatch Project Workflow Action */
|
||||
post: operations["dispatch_project_workflow_action_api_projects__project_id__workflow_actions_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/files/upload/": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -987,6 +1021,19 @@ export interface paths {
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
/** ActiveJobState */
|
||||
ActiveJobState: {
|
||||
/**
|
||||
* Job Id
|
||||
* Format: uuid
|
||||
*/
|
||||
job_id: string;
|
||||
/**
|
||||
* Job Type
|
||||
* @enum {string}
|
||||
*/
|
||||
job_type: "MEDIA_PROBE" | "SILENCE_REMOVE" | "SILENCE_DETECT" | "SILENCE_APPLY" | "MEDIA_CONVERT" | "TRANSCRIPTION_GENERATE" | "CAPTIONS_GENERATE" | "FRAME_EXTRACT";
|
||||
};
|
||||
/** ArtifactMediaFileCreate */
|
||||
ArtifactMediaFileCreate: {
|
||||
/** Project Id */
|
||||
@@ -1286,6 +1333,43 @@ export interface components {
|
||||
/** Result */
|
||||
result: string;
|
||||
};
|
||||
/** CaptionsState */
|
||||
CaptionsState: {
|
||||
/** @default IDLE */
|
||||
status: components["schemas"]["CaptionsWorkflowStatus"];
|
||||
/** Preset Id */
|
||||
preset_id?: string | null;
|
||||
/** Style Config */
|
||||
style_config?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
/** Render Job Id */
|
||||
render_job_id?: string | null;
|
||||
/** Output File Id */
|
||||
output_file_id?: string | null;
|
||||
};
|
||||
/**
|
||||
* CaptionsWorkflowStatus
|
||||
* @enum {string}
|
||||
*/
|
||||
CaptionsWorkflowStatus: "IDLE" | "CONFIGURED" | "PROCESSING" | "COMPLETED";
|
||||
/** ConfirmVerifyAction */
|
||||
ConfirmVerifyAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "CONFIRM_VERIFY";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
};
|
||||
/** CutRegionState */
|
||||
CutRegionState: {
|
||||
/** Start Ms */
|
||||
start_ms: number;
|
||||
/** End Ms */
|
||||
end_ms: number;
|
||||
};
|
||||
/** DispositionSchema */
|
||||
DispositionSchema: {
|
||||
/** Default */
|
||||
@@ -1653,6 +1737,16 @@ export interface components {
|
||||
/** Words */
|
||||
words: components["schemas"]["WordNode"][];
|
||||
};
|
||||
/** MarkTranscriptionReviewedAction */
|
||||
MarkTranscriptionReviewedAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "MARK_TRANSCRIPTION_REVIEWED";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
};
|
||||
/**
|
||||
* MediaConvertRequest
|
||||
* @description Request to convert media file to different format.
|
||||
@@ -1909,10 +2003,6 @@ export interface components {
|
||||
* @enum {string}
|
||||
*/
|
||||
status: "DRAFT" | "PROCESSING" | "DONE" | "FAILED";
|
||||
/** Workspace State */
|
||||
workspace_state: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
/** Is Active */
|
||||
is_active: boolean;
|
||||
/**
|
||||
@@ -1938,10 +2028,71 @@ export interface components {
|
||||
folder?: string | null;
|
||||
/** Status */
|
||||
status?: ("DRAFT" | "PROCESSING" | "DONE" | "FAILED") | null;
|
||||
/** Workspace State */
|
||||
workspace_state?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
/** ProjectWorkspaceRead */
|
||||
ProjectWorkspaceRead: {
|
||||
/**
|
||||
* Project Id
|
||||
* Format: uuid
|
||||
*/
|
||||
project_id: string;
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/** Version */
|
||||
version: number;
|
||||
phase: components["schemas"]["WorkflowPhase"];
|
||||
/**
|
||||
* Current Screen
|
||||
* @enum {string}
|
||||
*/
|
||||
current_screen: "upload" | "verify" | "silence-settings" | "processing" | "fragments" | "silence-apply-processing" | "transcription-settings" | "transcription-processing" | "subtitle-revision" | "caption-settings" | "caption-processing" | "caption-result";
|
||||
active_job: components["schemas"]["ActiveJobState"] | null;
|
||||
/** Source File Id */
|
||||
source_file_id: string | null;
|
||||
workspace_view: components["schemas"]["WorkspaceViewState"];
|
||||
silence: components["schemas"]["SilenceState"];
|
||||
transcription: components["schemas"]["TranscriptionState"];
|
||||
captions: components["schemas"]["CaptionsState"];
|
||||
};
|
||||
/** ReopenCaptionConfigAction */
|
||||
ReopenCaptionConfigAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "REOPEN_CAPTION_CONFIG";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
};
|
||||
/** ReopenSilenceReviewAction */
|
||||
ReopenSilenceReviewAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "REOPEN_SILENCE_REVIEW";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
};
|
||||
/** ReopenTranscriptionConfigAction */
|
||||
ReopenTranscriptionConfigAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "REOPEN_TRANSCRIPTION_CONFIG";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
};
|
||||
/** ResetSourceFileAction */
|
||||
ResetSourceFileAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "RESET_SOURCE_FILE";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
};
|
||||
/** SaluteSpeechParams */
|
||||
SaluteSpeechParams: {
|
||||
@@ -1979,6 +2130,71 @@ export interface components {
|
||||
/** Lines */
|
||||
lines: components["schemas"]["LineNode-Output"][];
|
||||
};
|
||||
/** SelectCaptionPresetAction */
|
||||
SelectCaptionPresetAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "SELECT_CAPTION_PRESET";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/** Preset Id */
|
||||
preset_id?: string | null;
|
||||
/** Style Config */
|
||||
style_config?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
/** SetSilenceCutsAction */
|
||||
SetSilenceCutsAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "SET_SILENCE_CUTS";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/** Cuts */
|
||||
cuts: components["schemas"]["CutRegionState"][];
|
||||
};
|
||||
/** SetSilenceSettingsAction */
|
||||
SetSilenceSettingsAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "SET_SILENCE_SETTINGS";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
settings?: components["schemas"]["SilenceSettingsState"];
|
||||
};
|
||||
/** SetSourceFileAction */
|
||||
SetSourceFileAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "SET_SOURCE_FILE";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/**
|
||||
* File Id
|
||||
* Format: uuid
|
||||
*/
|
||||
file_id: string;
|
||||
};
|
||||
/** SetWorkspaceViewAction */
|
||||
SetWorkspaceViewAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "SET_WORKSPACE_VIEW";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
workspace_view: components["schemas"]["WorkspaceViewState"];
|
||||
};
|
||||
/**
|
||||
* SilenceApplyRequest
|
||||
* @description Request to apply silence cuts to media file.
|
||||
@@ -2085,6 +2301,143 @@ export interface components {
|
||||
*/
|
||||
padding_ms: number;
|
||||
};
|
||||
/** SilenceSettingsState */
|
||||
SilenceSettingsState: {
|
||||
/**
|
||||
* Min Silence Duration Ms
|
||||
* @default 200
|
||||
*/
|
||||
min_silence_duration_ms: number;
|
||||
/**
|
||||
* Silence Threshold Db
|
||||
* @default 16
|
||||
*/
|
||||
silence_threshold_db: number;
|
||||
/**
|
||||
* Padding Ms
|
||||
* @default 100
|
||||
*/
|
||||
padding_ms: number;
|
||||
};
|
||||
/** SilenceState */
|
||||
SilenceState: {
|
||||
/** @default IDLE */
|
||||
status: components["schemas"]["SilenceWorkflowStatus"];
|
||||
settings?: components["schemas"]["SilenceSettingsState"];
|
||||
/** Detect Job Id */
|
||||
detect_job_id?: string | null;
|
||||
/** Detected Segments */
|
||||
detected_segments?: components["schemas"]["CutRegionState"][];
|
||||
/** Reviewed Cuts */
|
||||
reviewed_cuts?: components["schemas"]["CutRegionState"][];
|
||||
/** Duration Ms */
|
||||
duration_ms?: number | null;
|
||||
/** Applied Output File Id */
|
||||
applied_output_file_id?: string | null;
|
||||
};
|
||||
/**
|
||||
* SilenceWorkflowStatus
|
||||
* @enum {string}
|
||||
*/
|
||||
SilenceWorkflowStatus: "IDLE" | "CONFIGURED" | "DETECTING" | "REVIEWING" | "APPLYING" | "COMPLETED" | "SKIPPED";
|
||||
/** SkipSilenceApplyAction */
|
||||
SkipSilenceApplyAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "SKIP_SILENCE_APPLY";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
};
|
||||
/** StartCaptionRenderAction */
|
||||
StartCaptionRenderAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "START_CAPTION_RENDER";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/**
|
||||
* Folder
|
||||
* @default output_files
|
||||
*/
|
||||
folder: string;
|
||||
};
|
||||
/** StartMediaConvertAction */
|
||||
StartMediaConvertAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "START_MEDIA_CONVERT";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/**
|
||||
* Output Format
|
||||
* @default mp4
|
||||
*/
|
||||
output_format: string;
|
||||
/**
|
||||
* Out Folder
|
||||
* @default output_files
|
||||
*/
|
||||
out_folder: string;
|
||||
};
|
||||
/** StartSilenceApplyAction */
|
||||
StartSilenceApplyAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "START_SILENCE_APPLY";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/** Cuts */
|
||||
cuts?: components["schemas"]["CutRegionState"][] | null;
|
||||
/**
|
||||
* Out Folder
|
||||
* @default output_files
|
||||
*/
|
||||
out_folder: string;
|
||||
/** Output Name */
|
||||
output_name?: string | null;
|
||||
};
|
||||
/** StartSilenceDetectAction */
|
||||
StartSilenceDetectAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "START_SILENCE_DETECT";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
};
|
||||
/** StartTranscriptionAction */
|
||||
StartTranscriptionAction: {
|
||||
/**
|
||||
* @description discriminator enum property added by openapi-typescript
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "START_TRANSCRIPTION";
|
||||
/** Revision */
|
||||
revision: number;
|
||||
/**
|
||||
* Engine
|
||||
* @default whisper
|
||||
* @enum {string}
|
||||
*/
|
||||
engine: "whisper" | "google" | "salutespeech";
|
||||
/** Language */
|
||||
language?: string | null;
|
||||
/**
|
||||
* Model
|
||||
* @default base
|
||||
*/
|
||||
model: string;
|
||||
request?: components["schemas"]["TranscriptionRequestState"] | null;
|
||||
};
|
||||
/** StreamSchema */
|
||||
StreamSchema: {
|
||||
/** Index */
|
||||
@@ -2324,6 +2677,39 @@ export interface components {
|
||||
*/
|
||||
updated_at: string;
|
||||
};
|
||||
/** TranscriptionRequestState */
|
||||
TranscriptionRequestState: {
|
||||
/**
|
||||
* Engine
|
||||
* @default whisper
|
||||
* @enum {string}
|
||||
*/
|
||||
engine: "whisper" | "google" | "salutespeech";
|
||||
/** Language */
|
||||
language?: string | null;
|
||||
/**
|
||||
* Model
|
||||
* @default base
|
||||
*/
|
||||
model: string;
|
||||
};
|
||||
/** TranscriptionState */
|
||||
TranscriptionState: {
|
||||
/** @default IDLE */
|
||||
status: components["schemas"]["TranscriptionWorkflowStatus"];
|
||||
request?: components["schemas"]["TranscriptionRequestState"];
|
||||
/** Job Id */
|
||||
job_id?: string | null;
|
||||
/** Artifact Id */
|
||||
artifact_id?: string | null;
|
||||
/** Transcription Id */
|
||||
transcription_id?: string | null;
|
||||
/**
|
||||
* Reviewed
|
||||
* @default false
|
||||
*/
|
||||
reviewed: boolean;
|
||||
};
|
||||
/** TranscriptionUpdate */
|
||||
TranscriptionUpdate: {
|
||||
/** Document */
|
||||
@@ -2335,6 +2721,11 @@ export interface components {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
/**
|
||||
* TranscriptionWorkflowStatus
|
||||
* @enum {string}
|
||||
*/
|
||||
TranscriptionWorkflowStatus: "IDLE" | "PROCESSING" | "REVIEWING" | "COMPLETED";
|
||||
/** UserCreate */
|
||||
UserCreate: {
|
||||
/** Username */
|
||||
@@ -2542,6 +2933,18 @@ export interface components {
|
||||
structure_tags: components["schemas"]["Tag"][];
|
||||
time: components["schemas"]["TimeRange"];
|
||||
};
|
||||
/**
|
||||
* WorkflowPhase
|
||||
* @enum {string}
|
||||
*/
|
||||
WorkflowPhase: "INGEST" | "VERIFY" | "SILENCE" | "TRANSCRIPTION" | "CAPTIONS" | "DONE";
|
||||
/** WorkspaceViewState */
|
||||
WorkspaceViewState: {
|
||||
/** Used File Ids */
|
||||
used_file_ids?: string[];
|
||||
/** Selected File Id */
|
||||
selected_file_id?: string | null;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@@ -3055,6 +3458,72 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
get_project_workspace_api_projects__project_id__workspace_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
project_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProjectWorkspaceRead"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
dispatch_project_workflow_action_api_projects__project_id__workflow_actions_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
project_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SetSourceFileAction"] | components["schemas"]["ResetSourceFileAction"] | components["schemas"]["StartMediaConvertAction"] | components["schemas"]["ConfirmVerifyAction"] | components["schemas"]["SetSilenceSettingsAction"] | components["schemas"]["StartSilenceDetectAction"] | components["schemas"]["SetSilenceCutsAction"] | components["schemas"]["SkipSilenceApplyAction"] | components["schemas"]["StartSilenceApplyAction"] | components["schemas"]["ReopenSilenceReviewAction"] | components["schemas"]["StartTranscriptionAction"] | components["schemas"]["ReopenTranscriptionConfigAction"] | components["schemas"]["MarkTranscriptionReviewedAction"] | components["schemas"]["SelectCaptionPresetAction"] | components["schemas"]["StartCaptionRenderAction"] | components["schemas"]["ReopenCaptionConfigAction"] | components["schemas"]["SetWorkspaceViewAction"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProjectWorkspaceRead"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
upload_file_api_files_upload__post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
"use client"
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
import { ACCESS_TOKEN_REGEXP, API_URL } from "@shared/lib/constants"
|
||||
|
||||
export type WorkflowPhase =
|
||||
| "INGEST"
|
||||
| "VERIFY"
|
||||
| "SILENCE"
|
||||
| "TRANSCRIPTION"
|
||||
| "CAPTIONS"
|
||||
| "DONE"
|
||||
|
||||
export type WorkflowScreen =
|
||||
| "upload"
|
||||
| "verify"
|
||||
| "silence-settings"
|
||||
| "processing"
|
||||
| "fragments"
|
||||
| "silence-apply-processing"
|
||||
| "transcription-settings"
|
||||
| "transcription-processing"
|
||||
| "subtitle-revision"
|
||||
| "caption-settings"
|
||||
| "caption-processing"
|
||||
| "caption-result"
|
||||
|
||||
export interface SilenceSettingsPayload {
|
||||
min_silence_duration_ms: number
|
||||
silence_threshold_db: number
|
||||
padding_ms: number
|
||||
}
|
||||
|
||||
export interface WorkflowCutRegionPayload {
|
||||
start_ms: number
|
||||
end_ms: number
|
||||
}
|
||||
|
||||
export interface WorkflowActiveJob {
|
||||
job_id: string
|
||||
job_type: string
|
||||
}
|
||||
|
||||
export interface WorkflowWorkspaceView {
|
||||
used_file_ids: string[]
|
||||
selected_file_id: string | null
|
||||
}
|
||||
|
||||
export interface WorkflowSilenceState {
|
||||
status: string | null
|
||||
settings: SilenceSettingsPayload | null
|
||||
detect_job_id: string | null
|
||||
detected_segments: WorkflowCutRegionPayload[]
|
||||
reviewed_cuts: WorkflowCutRegionPayload[]
|
||||
duration_ms: number | null
|
||||
applied_output_file_id: string | null
|
||||
}
|
||||
|
||||
export interface WorkflowTranscriptionRequest {
|
||||
engine: "whisper" | "google" | "salutespeech"
|
||||
language?: string
|
||||
model: string
|
||||
}
|
||||
|
||||
export interface WorkflowTranscriptionState {
|
||||
status: string | null
|
||||
job_id: string | null
|
||||
request: WorkflowTranscriptionRequest | null
|
||||
artifact_id: string | null
|
||||
transcription_id: string | null
|
||||
reviewed: boolean
|
||||
}
|
||||
|
||||
export interface WorkflowCaptionsState {
|
||||
status: string | null
|
||||
preset_id: string | null
|
||||
style_config: Record<string, unknown> | null
|
||||
render_job_id: string | null
|
||||
output_file_id: string | null
|
||||
}
|
||||
|
||||
export interface ProjectWorkspaceRead {
|
||||
revision: number
|
||||
phase: WorkflowPhase
|
||||
current_screen: WorkflowScreen
|
||||
active_job: WorkflowActiveJob | null
|
||||
source_file_id: string | null
|
||||
workspace_view: WorkflowWorkspaceView
|
||||
silence: WorkflowSilenceState
|
||||
transcription: WorkflowTranscriptionState
|
||||
captions: WorkflowCaptionsState
|
||||
}
|
||||
|
||||
type WorkflowActionBase<TActionType extends string> = {
|
||||
type: TActionType
|
||||
revision: number
|
||||
}
|
||||
|
||||
export type WorkflowActionRequest =
|
||||
| (WorkflowActionBase<"SET_SOURCE_FILE"> & {
|
||||
file_id: string
|
||||
})
|
||||
| WorkflowActionBase<"RESET_SOURCE_FILE">
|
||||
| (WorkflowActionBase<"START_MEDIA_CONVERT"> & {
|
||||
output_format?: "mp4"
|
||||
})
|
||||
| WorkflowActionBase<"CONFIRM_VERIFY">
|
||||
| (WorkflowActionBase<"SET_SILENCE_SETTINGS"> & {
|
||||
settings: SilenceSettingsPayload
|
||||
})
|
||||
| WorkflowActionBase<"START_SILENCE_DETECT">
|
||||
| (WorkflowActionBase<"SET_SILENCE_CUTS"> & {
|
||||
cuts: WorkflowCutRegionPayload[]
|
||||
})
|
||||
| WorkflowActionBase<"SKIP_SILENCE_APPLY">
|
||||
| (WorkflowActionBase<"START_SILENCE_APPLY"> & {
|
||||
cuts: WorkflowCutRegionPayload[]
|
||||
})
|
||||
| WorkflowActionBase<"REOPEN_SILENCE_REVIEW">
|
||||
| (WorkflowActionBase<"START_TRANSCRIPTION"> & {
|
||||
request: WorkflowTranscriptionRequest
|
||||
})
|
||||
| WorkflowActionBase<"REOPEN_TRANSCRIPTION_CONFIG">
|
||||
| WorkflowActionBase<"MARK_TRANSCRIPTION_REVIEWED">
|
||||
| (WorkflowActionBase<"SELECT_CAPTION_PRESET"> & {
|
||||
preset_id: string | null
|
||||
})
|
||||
| WorkflowActionBase<"START_CAPTION_RENDER">
|
||||
| WorkflowActionBase<"REOPEN_CAPTION_CONFIG">
|
||||
| (WorkflowActionBase<"SET_WORKSPACE_VIEW"> & {
|
||||
workspace_view: WorkflowWorkspaceView
|
||||
})
|
||||
|
||||
class WorkflowApiError extends Error {
|
||||
status: number
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.name = "WorkflowApiError"
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseApiUrl(): string {
|
||||
if (API_URL?.length) return API_URL
|
||||
if (typeof window !== "undefined") return window.location.origin
|
||||
return ""
|
||||
}
|
||||
|
||||
function getAccessToken(): string | null {
|
||||
if (typeof document === "undefined") return null
|
||||
const token = document.cookie.replace(ACCESS_TOKEN_REGEXP, "$1")
|
||||
return token.length ? token : null
|
||||
}
|
||||
|
||||
async function requestJson<TResponse>(
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
): Promise<TResponse> {
|
||||
const token = getAccessToken()
|
||||
const response = await fetch(`${getBaseApiUrl()}${path}`, {
|
||||
credentials: "include",
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const message = response.statusText || "Workflow request failed"
|
||||
throw new WorkflowApiError(response.status, message)
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return null as TResponse
|
||||
}
|
||||
|
||||
return (await response.json()) as TResponse
|
||||
}
|
||||
|
||||
export function getProjectWorkspaceQueryKey(projectId: string) {
|
||||
return ["project-workspace", projectId] as const
|
||||
}
|
||||
|
||||
export async function fetchProjectWorkspace(
|
||||
projectId: string,
|
||||
): Promise<ProjectWorkspaceRead> {
|
||||
return requestJson<ProjectWorkspaceRead>(
|
||||
`/api/projects/${projectId}/workspace`,
|
||||
{ method: "GET" },
|
||||
)
|
||||
}
|
||||
|
||||
export async function postWorkflowAction(
|
||||
projectId: string,
|
||||
action: WorkflowActionRequest,
|
||||
): Promise<ProjectWorkspaceRead | null> {
|
||||
return requestJson<ProjectWorkspaceRead | null>(
|
||||
`/api/projects/${projectId}/workflow/actions`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(action),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function useProjectWorkspaceQuery(projectId: string) {
|
||||
return useQuery({
|
||||
queryKey: getProjectWorkspaceQueryKey(projectId),
|
||||
queryFn: () => fetchProjectWorkspace(projectId),
|
||||
enabled: !!projectId,
|
||||
})
|
||||
}
|
||||
|
||||
export function useWorkflowAction(projectId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
const queryKey = getProjectWorkspaceQueryKey(projectId)
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (action: WorkflowActionRequest) =>
|
||||
postWorkflowAction(projectId, action),
|
||||
onSuccess: (workspace) => {
|
||||
if (workspace) {
|
||||
queryClient.setQueryData(queryKey, workspace)
|
||||
return
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
},
|
||||
onError: (error) => {
|
||||
if (
|
||||
error instanceof WorkflowApiError &&
|
||||
error.status === 409
|
||||
) {
|
||||
queryClient.invalidateQueries({ queryKey })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function isWorkflowConflictError(error: unknown): boolean {
|
||||
return error instanceof WorkflowApiError && error.status === 409
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
NotificationItem,
|
||||
setNotifications,
|
||||
} from "@shared/store/notifications"
|
||||
import { getProjectWorkspaceQueryKey } from "@shared/api/projectWorkflow"
|
||||
|
||||
interface SocketContextValue {
|
||||
isConnected: boolean
|
||||
@@ -246,6 +247,12 @@ export const SocketProvider = ({
|
||||
queryKey: ["get", "/api/files/files/"],
|
||||
})
|
||||
}
|
||||
|
||||
if (data.project_id) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getProjectWorkspaceQueryKey(data.project_id),
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed messages
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,12 +13,13 @@ import {
|
||||
} from "react"
|
||||
|
||||
import api from "@shared/api"
|
||||
import {
|
||||
type WorkflowWorkspaceView,
|
||||
useProjectWorkspaceQuery,
|
||||
useWorkflowAction,
|
||||
} from "@shared/api/projectWorkflow"
|
||||
import { useDebounce } from "@shared/hooks/useDebounce"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export type SelectedFile = {
|
||||
id: string
|
||||
path: string
|
||||
@@ -43,98 +44,182 @@ interface WorkspaceFileContextValue {
|
||||
isLoaded: boolean
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Context */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const FileContext = createContext<WorkspaceFileContextValue | null>(null)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Provider */
|
||||
/* ------------------------------------------------------------------ */
|
||||
const DEBOUNCE_MS = 300
|
||||
|
||||
const DEBOUNCE_MS = 1000
|
||||
function getFileIconType(mimeType: string | null | undefined) {
|
||||
if (!mimeType) return "other" as const
|
||||
if (mimeType.startsWith("video/")) return "video" as const
|
||||
if (mimeType.startsWith("audio/")) return "audio" as const
|
||||
if (mimeType.includes("json") || mimeType.startsWith("text/")) {
|
||||
return "text" as const
|
||||
}
|
||||
return "other" as const
|
||||
}
|
||||
|
||||
function getArtifactDisplayName(artifactType: string | null | undefined): string {
|
||||
switch (artifactType) {
|
||||
case "TRANSCRIPTION_JSON":
|
||||
return "Субтитры"
|
||||
default:
|
||||
return artifactType ?? "Артефакт"
|
||||
}
|
||||
}
|
||||
|
||||
export const WorkspaceProvider: FunctionComponent<{
|
||||
projectId: string
|
||||
children: ReactNode
|
||||
}> = ({ projectId, children }) => {
|
||||
const [selectedFile, setSelectedFileState] = useState<SelectedFile | null>(
|
||||
const { data: workspace } = useProjectWorkspaceQuery(projectId)
|
||||
const workflowAction = useWorkflowAction(projectId)
|
||||
|
||||
const [usedFileIds, setUsedFileIds] = useState<string[]>([])
|
||||
const [selectedPersistedId, setSelectedPersistedId] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
const [usedFiles, setUsedFiles] = useState<UsedFile[]>([])
|
||||
const isInitializedRef = useRef(false)
|
||||
const initialValueRef = useRef<string | null>(null)
|
||||
const [selectedFile, setSelectedFileState] = useState<SelectedFile | null>(null)
|
||||
const latestRevisionRef = useRef<number | null>(null)
|
||||
|
||||
/* ---- Load from server ---- */
|
||||
useEffect(() => {
|
||||
if (!workspace) return
|
||||
|
||||
const { data: project, isSuccess } = api.useQuery(
|
||||
"get",
|
||||
"/api/projects/{project_id}/",
|
||||
{ params: { path: { project_id: projectId } } },
|
||||
{ enabled: !!projectId },
|
||||
if (latestRevisionRef.current === workspace.revision) {
|
||||
return
|
||||
}
|
||||
|
||||
latestRevisionRef.current = workspace.revision
|
||||
setUsedFileIds(workspace.workspace_view.used_file_ids)
|
||||
setSelectedPersistedId(workspace.workspace_view.selected_file_id)
|
||||
}, [workspace])
|
||||
|
||||
const { data: files } = api.useQuery("get", "/api/files/files/", {})
|
||||
const { data: artifacts } = api.useQuery("get", "/api/media/artifacts/", {})
|
||||
|
||||
const fileMap = useMemo(() => {
|
||||
const nextMap = new Map<string, UsedFile>()
|
||||
|
||||
for (const file of files ?? []) {
|
||||
if (file.project_id !== projectId || file.is_deleted) continue
|
||||
|
||||
nextMap.set(file.id, {
|
||||
id: file.id,
|
||||
path: file.path,
|
||||
source: "file",
|
||||
mimeType: file.mime_type,
|
||||
displayName: file.original_filename,
|
||||
iconType: getFileIconType(file.mime_type),
|
||||
})
|
||||
}
|
||||
|
||||
return nextMap
|
||||
}, [files, projectId])
|
||||
|
||||
const artifactMap = useMemo(() => {
|
||||
const nextMap = new Map<string, UsedFile>()
|
||||
|
||||
for (const artifact of artifacts ?? []) {
|
||||
if (artifact.project_id !== projectId || artifact.is_deleted) continue
|
||||
|
||||
nextMap.set(artifact.id, {
|
||||
id: artifact.id,
|
||||
path: "transcription",
|
||||
source: "artifact",
|
||||
artifactType: artifact.artifact_type,
|
||||
displayName: getArtifactDisplayName(artifact.artifact_type),
|
||||
iconType:
|
||||
artifact.artifact_type === "TRANSCRIPTION_JSON" ? "text" : "other",
|
||||
})
|
||||
}
|
||||
|
||||
return nextMap
|
||||
}, [artifacts, projectId])
|
||||
|
||||
const resolveUsedFile = useCallback(
|
||||
(fileId: string, previous?: UsedFile | null): UsedFile | null => {
|
||||
return fileMap.get(fileId) ?? artifactMap.get(fileId) ?? previous ?? null
|
||||
},
|
||||
[fileMap, artifactMap],
|
||||
)
|
||||
|
||||
const usedFiles = useMemo(
|
||||
() =>
|
||||
usedFileIds
|
||||
.map((fileId) => resolveUsedFile(fileId))
|
||||
.filter((file): file is UsedFile => file !== null),
|
||||
[resolveUsedFile, usedFileIds],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSuccess || isInitializedRef.current) return
|
||||
setSelectedFileState((prev) => {
|
||||
if (!selectedPersistedId) return null
|
||||
|
||||
const saved = project?.workspace_state as
|
||||
| { used_files?: UsedFile[] }
|
||||
| null
|
||||
| undefined
|
||||
const loaded = saved?.used_files ?? []
|
||||
const resolved = resolveUsedFile(
|
||||
selectedPersistedId,
|
||||
prev as UsedFile | null,
|
||||
)
|
||||
if (!resolved) return prev
|
||||
|
||||
setUsedFiles(loaded)
|
||||
initialValueRef.current = JSON.stringify(loaded)
|
||||
isInitializedRef.current = true
|
||||
}, [isSuccess, project])
|
||||
if (prev?.id === selectedPersistedId) {
|
||||
return {
|
||||
...resolved,
|
||||
scrollToSegmentIndex: prev.scrollToSegmentIndex,
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Save to server (debounced) ---- */
|
||||
|
||||
const debouncedUsedFiles = useDebounce(usedFiles, DEBOUNCE_MS)
|
||||
|
||||
const saveMutation = api.useMutation(
|
||||
"patch",
|
||||
"/api/projects/{project_id}/",
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitializedRef.current) return
|
||||
|
||||
const serialized = JSON.stringify(debouncedUsedFiles)
|
||||
if (serialized === initialValueRef.current) return
|
||||
|
||||
initialValueRef.current = serialized
|
||||
saveMutation.mutate({
|
||||
params: { path: { project_id: projectId } },
|
||||
body: {
|
||||
workspace_state: { used_files: debouncedUsedFiles },
|
||||
},
|
||||
return resolved
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedUsedFiles, projectId])
|
||||
}, [resolveUsedFile, selectedPersistedId])
|
||||
|
||||
/* ---- Actions ---- */
|
||||
|
||||
const setSelectedFile = useCallback(
|
||||
(file: SelectedFile | null) => setSelectedFileState(file),
|
||||
[],
|
||||
const persistableWorkspaceView = useMemo<WorkflowWorkspaceView>(
|
||||
() => ({
|
||||
used_file_ids: usedFileIds,
|
||||
selected_file_id: selectedPersistedId,
|
||||
}),
|
||||
[selectedPersistedId, usedFileIds],
|
||||
)
|
||||
|
||||
const debouncedWorkspaceView = useDebounce(
|
||||
persistableWorkspaceView,
|
||||
DEBOUNCE_MS,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspace) return
|
||||
|
||||
const localSignature = JSON.stringify(debouncedWorkspaceView)
|
||||
const serverSignature = JSON.stringify(workspace.workspace_view)
|
||||
|
||||
if (localSignature === serverSignature) return
|
||||
|
||||
void workflowAction.mutateAsync({
|
||||
type: "SET_WORKSPACE_VIEW",
|
||||
revision: workspace.revision,
|
||||
workspace_view: debouncedWorkspaceView,
|
||||
})
|
||||
}, [debouncedWorkspaceView, workflowAction, workspace])
|
||||
|
||||
const setSelectedFile = useCallback((file: SelectedFile | null) => {
|
||||
setSelectedFileState(file)
|
||||
setSelectedPersistedId(file?.id ?? null)
|
||||
}, [])
|
||||
|
||||
const addUsedFile = useCallback((file: UsedFile) => {
|
||||
setUsedFiles((prev) => {
|
||||
if (prev.some((f) => f.id === file.id)) return prev
|
||||
return [...prev, file]
|
||||
setUsedFileIds((prev) => {
|
||||
if (prev.includes(file.id)) return prev
|
||||
return [...prev, file.id]
|
||||
})
|
||||
}, [])
|
||||
|
||||
const removeUsedFile = useCallback((id: string) => {
|
||||
setUsedFiles((prev) => prev.filter((f) => f.id !== id))
|
||||
setUsedFileIds((prev) => prev.filter((fileId) => fileId !== id))
|
||||
setSelectedPersistedId((prev) => (prev === id ? null : prev))
|
||||
setSelectedFileState((prev) => (prev?.id === id ? null : prev))
|
||||
}, [])
|
||||
|
||||
const isFileUsed = useCallback(
|
||||
(id: string) => usedFiles.some((f) => f.id === id),
|
||||
[usedFiles],
|
||||
(id: string) => usedFileIds.includes(id),
|
||||
[usedFileIds],
|
||||
)
|
||||
|
||||
const value = useMemo<WorkspaceFileContextValue>(
|
||||
@@ -145,82 +230,22 @@ export const WorkspaceProvider: FunctionComponent<{
|
||||
addUsedFile,
|
||||
removeUsedFile,
|
||||
isFileUsed,
|
||||
isLoaded: isInitializedRef.current,
|
||||
isLoaded: Boolean(workspace),
|
||||
}),
|
||||
[
|
||||
addUsedFile,
|
||||
isFileUsed,
|
||||
removeUsedFile,
|
||||
selectedFile,
|
||||
setSelectedFile,
|
||||
usedFiles,
|
||||
addUsedFile,
|
||||
removeUsedFile,
|
||||
isFileUsed,
|
||||
workspace,
|
||||
],
|
||||
)
|
||||
|
||||
return <FileContext.Provider value={value}>{children}</FileContext.Provider>
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Static provider (in-memory only, no server persistence) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export const StaticWorkspaceProvider: FunctionComponent<{
|
||||
children: ReactNode
|
||||
}> = ({ children }) => {
|
||||
const [selectedFile, setSelectedFileState] = useState<SelectedFile | null>(
|
||||
null,
|
||||
)
|
||||
const [usedFiles, setUsedFiles] = useState<UsedFile[]>([])
|
||||
|
||||
const setSelectedFile = useCallback(
|
||||
(file: SelectedFile | null) => setSelectedFileState(file),
|
||||
[],
|
||||
)
|
||||
|
||||
const addUsedFile = useCallback((file: UsedFile) => {
|
||||
setUsedFiles((prev) => {
|
||||
if (prev.some((f) => f.id === file.id)) return prev
|
||||
return [...prev, file]
|
||||
})
|
||||
}, [])
|
||||
|
||||
const removeUsedFile = useCallback((id: string) => {
|
||||
setUsedFiles((prev) => prev.filter((f) => f.id !== id))
|
||||
}, [])
|
||||
|
||||
const isFileUsed = useCallback(
|
||||
(id: string) => usedFiles.some((f) => f.id === id),
|
||||
[usedFiles],
|
||||
)
|
||||
|
||||
const value = useMemo<WorkspaceFileContextValue>(
|
||||
() => ({
|
||||
selectedFile,
|
||||
setSelectedFile,
|
||||
usedFiles,
|
||||
addUsedFile,
|
||||
removeUsedFile,
|
||||
isFileUsed,
|
||||
isLoaded: true,
|
||||
}),
|
||||
[
|
||||
selectedFile,
|
||||
setSelectedFile,
|
||||
usedFiles,
|
||||
addUsedFile,
|
||||
removeUsedFile,
|
||||
isFileUsed,
|
||||
],
|
||||
)
|
||||
|
||||
return <FileContext.Provider value={value}>{children}</FileContext.Provider>
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Hook */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** File selection & used-files list — stable during playback */
|
||||
export function useWorkspaceFiles(): WorkspaceFileContextValue {
|
||||
const ctx = useContext(FileContext)
|
||||
if (!ctx) {
|
||||
|
||||
Reference in New Issue
Block a user