-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathdubSlice.ts
More file actions
187 lines (168 loc) · 7.98 KB
/
Copy pathdubSlice.ts
File metadata and controls
187 lines (168 loc) · 7.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/**
* Dub pipeline slice — Phase 2.2 (App.jsx monolith reduction).
*
* This was the largest cluster of `useState` calls in App.jsx — the state
* that drives the whole dubbing workflow: job id, progress, segments,
* language, translate/generate settings. Moving it into the store lets deep
* children read pipeline state without 30+ props threaded through.
*
* Setters mirror React's signature — each accepts either a value or an
* `(prev) => next` updater so existing call sites (`setDubSegments(prev =>
* prev.map(...))`) work unchanged. The `functionalSet` helper keeps that
* surface tiny.
*
* Not persisted: dub state is transient per session. Project load / dub
* history restore explicitly rehydrates the relevant fields.
*/
import type { StateCreator } from 'zustand';
import type { EffectPreset } from '../api/engines';
export type DubStep =
| 'idle'
| 'uploading'
| 'transcribing'
| 'editing'
| 'generating'
| 'stopping'
| 'done';
export type DubPrepStage = 'download' | 'extract' | 'demucs' | 'scene' | null;
export interface DubProgress {
current: number;
total: number;
text: string;
}
/** Segments are a loose shape — many optional fields added over time. */
export type DubSegment = Record<string, unknown> & { id: string; text: string };
type Updater<T> = T | ((prev: T) => T);
function resolve<T>(updater: Updater<T>, prev: T): T {
return typeof updater === 'function' ? (updater as (prev: T) => T)(prev) : updater;
}
export interface DubSlice {
// ── Pipeline state ────────────────────────────────────────────────────
dubJobId: string | null;
dubStep: DubStep;
dubTaskId: string | null;
dubPrepStage: DubPrepStage;
dubProgress: DubProgress;
dubError: string;
isTranslating: boolean;
// ── Content ───────────────────────────────────────────────────────────
dubSegments: DubSegment[];
dubTranscript: string;
dubFilename: string;
dubDuration: number;
dubTracks: string[];
// ── Language / translate ──────────────────────────────────────────────
dubLang: string;
dubLangCode: string;
// ── Generation options ────────────────────────────────────────────────
dubInstruct: string;
preserveBg: boolean;
defaultTrack: string;
exportTracks: Record<string, boolean>;
// Segment ids most recently rendered at num_step=8 (preview quality).
// The client re-renders these at full quality before final export.
previewSegIds: string[];
// Per-speaker auto-clones extracted from the source video's vocals. Keys
// are speaker_id (e.g. "Speaker 1"), values are {ref_audio, ref_text,
// duration, source_count}. Enables the cross-lingual "same voice in a
// new language" dubbing flow.
speakerClones: Record<string, {
ref_audio: string;
ref_text: string;
duration: number;
source_count: number;
}>;
// ── Effect Presets ────────────────────────────────────────────────────
segmentEffectPresets: Record<string, string>;
setSegmentEffectPreset: (segId: string, presetId: string) => void;
availableEffectPresets: EffectPreset[];
setAvailableEffectPresets: (presets: EffectPreset[]) => void;
// ── Setters (React-style; accept value or updater fn) ─────────────────
setDubJobId: (v: Updater<string | null>) => void;
setDubStep: (v: Updater<DubStep>) => void;
setDubTaskId: (v: Updater<string | null>) => void;
setDubPrepStage: (v: Updater<DubPrepStage>) => void;
setDubProgress: (v: Updater<DubProgress>) => void;
setDubError: (v: Updater<string>) => void;
setIsTranslating: (v: Updater<boolean>) => void;
setDubSegments: (v: Updater<DubSegment[]>) => void;
setDubTranscript: (v: Updater<string>) => void;
setDubFilename: (v: Updater<string>) => void;
setDubDuration: (v: Updater<number>) => void;
setDubTracks: (v: Updater<string[]>) => void;
setDubLang: (v: Updater<string>) => void;
setDubLangCode: (v: Updater<string>) => void;
setDubInstruct: (v: Updater<string>) => void;
setPreserveBg: (v: Updater<boolean>) => void;
setDefaultTrack: (v: Updater<string>) => void;
setExportTracks: (v: Updater<Record<string, boolean>>) => void;
setPreviewSegIds: (v: Updater<string[]>) => void;
setSpeakerClones: (v: Updater<DubSlice['speakerClones']>) => void;
/** Reset every pipeline field back to idle defaults. */
resetDubState: () => void;
}
const INITIAL: Omit<DubSlice,
| 'setDubJobId' | 'setDubStep' | 'setDubTaskId' | 'setDubPrepStage'
| 'setDubProgress' | 'setDubError' | 'setIsTranslating' | 'setDubSegments'
| 'setDubTranscript' | 'setDubFilename' | 'setDubDuration' | 'setDubTracks'
| 'setDubLang' | 'setDubLangCode' | 'setDubInstruct' | 'setPreserveBg'
| 'setDefaultTrack' | 'setExportTracks' | 'setPreviewSegIds' | 'setSpeakerClones'
| 'setSegmentEffectPreset' | 'setAvailableEffectPresets' | 'resetDubState'
> = {
dubJobId: null,
dubStep: 'idle',
dubTaskId: null,
dubPrepStage: null,
dubProgress: { current: 0, total: 0, text: '' },
dubError: '',
isTranslating: false,
dubSegments: [],
dubTranscript: '',
dubFilename: '',
dubDuration: 0,
dubTracks: [],
dubLang: 'Auto',
dubLangCode: 'en',
dubInstruct: '',
preserveBg: true,
defaultTrack: 'original',
exportTracks: { original: true },
previewSegIds: [],
speakerClones: {},
segmentEffectPresets: {},
availableEffectPresets: [],
};
export const createDubSlice: StateCreator<DubSlice, [], [], DubSlice> = (set, get) => ({
...INITIAL,
setDubJobId: (v) => set((s) => ({ dubJobId: resolve(v, s.dubJobId) })),
setDubStep: (v) => set((s) => ({ dubStep: resolve(v, s.dubStep) })),
setDubTaskId: (v) => set((s) => ({ dubTaskId: resolve(v, s.dubTaskId) })),
setDubPrepStage: (v) => set((s) => ({ dubPrepStage: resolve(v, s.dubPrepStage) })),
setDubProgress: (v) => set((s) => ({ dubProgress: resolve(v, s.dubProgress) })),
setDubError: (v) => set((s) => ({ dubError: resolve(v, s.dubError) })),
setIsTranslating:(v) => set((s) => ({ isTranslating:resolve(v, s.isTranslating) })),
setDubSegments: (v) => set((s) => ({ dubSegments: resolve(v, s.dubSegments) })),
setDubTranscript:(v) => set((s) => ({ dubTranscript:resolve(v, s.dubTranscript) })),
setDubFilename: (v) => set((s) => ({ dubFilename: resolve(v, s.dubFilename) })),
setDubDuration: (v) => set((s) => ({ dubDuration: resolve(v, s.dubDuration) })),
setDubTracks: (v) => set((s) => ({ dubTracks: resolve(v, s.dubTracks) })),
setDubLang: (v) => set((s) => ({ dubLang: resolve(v, s.dubLang) })),
setDubLangCode: (v) => set((s) => ({ dubLangCode: resolve(v, s.dubLangCode) })),
setDubInstruct: (v) => set((s) => ({ dubInstruct: resolve(v, s.dubInstruct) })),
setPreserveBg: (v) => set((s) => ({ preserveBg: resolve(v, s.preserveBg) })),
setDefaultTrack: (v) => set((s) => ({ defaultTrack: resolve(v, s.defaultTrack) })),
setExportTracks: (v) => set((s) => ({ exportTracks: resolve(v, s.exportTracks) })),
setPreviewSegIds:(v) => set((s) => ({ previewSegIds:resolve(v, s.previewSegIds) })),
setSpeakerClones:(v) => set((s) => ({ speakerClones: resolve(v, s.speakerClones) })),
setSegmentEffectPreset: (segId, presetId) =>
set((s) => ({
segmentEffectPresets: { ...s.segmentEffectPresets, [segId]: presetId },
})),
setAvailableEffectPresets: (presets) => set({ availableEffectPresets: presets }),
resetDubState: () => {
// Touch `get` so strict-mode double-invocation of the initializer doesn't
// warn us about unused args — and future logging can read current state.
void get;
set(INITIAL);
},
});