mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 07:45:34 +00:00
Add export caption sidecar option
This commit is contained in:
@@ -29,6 +29,9 @@ interface ExportSettingsMenuProps {
|
||||
experimentalNvidiaCudaExport?: boolean;
|
||||
onExperimentalNvidiaCudaExportChange?: (enabled: boolean) => void;
|
||||
nvidiaCudaExportAvailable?: boolean;
|
||||
showCaptionSidecarOption?: boolean;
|
||||
includeCaptionSidecar?: boolean;
|
||||
onIncludeCaptionSidecarChange?: (enabled: boolean) => void;
|
||||
mp4OutputDimensions?: Record<ExportQuality, { width: number; height: number }>;
|
||||
gifFrameRate: GifFrameRate;
|
||||
onGifFrameRateChange?: (rate: GifFrameRate) => void;
|
||||
@@ -55,6 +58,9 @@ export function ExportSettingsMenu({
|
||||
experimentalNvidiaCudaExport = false,
|
||||
onExperimentalNvidiaCudaExportChange,
|
||||
nvidiaCudaExportAvailable = false,
|
||||
showCaptionSidecarOption = false,
|
||||
includeCaptionSidecar = false,
|
||||
onIncludeCaptionSidecarChange,
|
||||
mp4OutputDimensions,
|
||||
gifFrameRate,
|
||||
onGifFrameRateChange,
|
||||
@@ -365,6 +371,30 @@ export function ExportSettingsMenu({
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{showCaptionSidecarOption ? (
|
||||
<div className="mb-3 flex min-h-12 items-center justify-between gap-3 rounded-lg border border-foreground/10 bg-foreground/5 px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-semibold text-foreground">
|
||||
{tSettings("export.captionSidecar.title", "Export captions file")}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-[10px] text-muted-foreground/75">
|
||||
{tSettings(
|
||||
"export.captionSidecar.hint",
|
||||
"Save .srt and .vtt files next to your exported video.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={includeCaptionSidecar}
|
||||
onCheckedChange={onIncludeCaptionSidecarChange}
|
||||
aria-label={tSettings(
|
||||
"export.captionSidecar.toggle",
|
||||
"Export captions sidecar files",
|
||||
)}
|
||||
className="shrink-0 scale-75 data-[state=checked]:bg-[#2563EB]"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</LayoutGroup>
|
||||
) : (
|
||||
<div className="mb-3 space-y-2">
|
||||
|
||||
@@ -226,6 +226,14 @@ type PendingExportSave = {
|
||||
fileName: string;
|
||||
arrayBuffer?: ArrayBuffer;
|
||||
tempFilePath?: string;
|
||||
captionSidecar?: {
|
||||
format: "srt" | "vtt" | "both";
|
||||
cues: Array<{
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
text: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
type CancelableExporter = {
|
||||
@@ -521,6 +529,7 @@ export default function VideoEditor() {
|
||||
const [autoCaptionSettings, setAutoCaptionSettings] = useState<AutoCaptionSettings>(
|
||||
DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
);
|
||||
const [includeCaptionSidecar, setIncludeCaptionSidecar] = useState(true);
|
||||
const [whisperExecutablePath, setWhisperExecutablePath] = useState<string | null>(
|
||||
initialEditorPreferences.whisperExecutablePath,
|
||||
);
|
||||
@@ -596,6 +605,32 @@ export default function VideoEditor() {
|
||||
const [gifSizePreset, setGifSizePreset] = useState<GifSizePreset>(
|
||||
initialEditorPreferences.gifSizePreset,
|
||||
);
|
||||
const hasCaptionsForSidecar = autoCaptionSettings.enabled && autoCaptions.length > 0;
|
||||
const captionSidecarCues = useMemo(
|
||||
() =>
|
||||
autoCaptions
|
||||
.filter(
|
||||
(cue) =>
|
||||
Number.isFinite(cue.startMs) &&
|
||||
Number.isFinite(cue.endMs) &&
|
||||
cue.endMs > cue.startMs &&
|
||||
typeof cue.text === "string" &&
|
||||
cue.text.trim().length > 0,
|
||||
)
|
||||
.map((cue) => ({
|
||||
startMs: cue.startMs,
|
||||
endMs: cue.endMs,
|
||||
text: cue.text,
|
||||
})),
|
||||
[autoCaptions],
|
||||
);
|
||||
const captionSidecarPayload =
|
||||
hasCaptionsForSidecar && captionSidecarCues.length > 0 && includeCaptionSidecar
|
||||
? {
|
||||
format: "both" as const,
|
||||
cues: captionSidecarCues,
|
||||
}
|
||||
: undefined;
|
||||
const [exportedFilePath, setExportedFilePath] = useState<string | undefined>(undefined);
|
||||
const [hasPendingExportSave, setHasPendingExportSave] = useState(false);
|
||||
const [lastSavedSnapshot, setLastSavedSnapshot] = useState<EditorProjectData | null>(null);
|
||||
@@ -1283,7 +1318,12 @@ export default function VideoEditor() {
|
||||
}, []);
|
||||
|
||||
const saveBlobExport = useCallback(
|
||||
async (blob: Blob, fileName: string, outputPath: string | null = null) => {
|
||||
async (
|
||||
blob: Blob,
|
||||
fileName: string,
|
||||
outputPath: string | null = null,
|
||||
captionSidecar?: PendingExportSave["captionSidecar"],
|
||||
) => {
|
||||
const extension = fileName.split(".").pop()?.toLowerCase() || "bin";
|
||||
const hasExportStreamApi =
|
||||
typeof window !== "undefined" &&
|
||||
@@ -1300,10 +1340,12 @@ export default function VideoEditor() {
|
||||
tempPath: tempFilePath,
|
||||
fileName,
|
||||
outputPath,
|
||||
captionSidecar,
|
||||
}),
|
||||
pendingSave: {
|
||||
fileName,
|
||||
tempFilePath,
|
||||
captionSidecar,
|
||||
} satisfies PendingExportSave,
|
||||
};
|
||||
}
|
||||
@@ -1342,11 +1384,20 @@ export default function VideoEditor() {
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
return {
|
||||
saveResult: outputPath
|
||||
? await window.electronAPI.writeExportedVideoToPath(arrayBuffer, outputPath)
|
||||
: await window.electronAPI.saveExportedVideo(arrayBuffer, fileName),
|
||||
? await window.electronAPI.writeExportedVideoToPath(
|
||||
arrayBuffer,
|
||||
outputPath,
|
||||
captionSidecar,
|
||||
)
|
||||
: await window.electronAPI.saveExportedVideo(
|
||||
arrayBuffer,
|
||||
fileName,
|
||||
captionSidecar,
|
||||
),
|
||||
pendingSave: {
|
||||
fileName,
|
||||
arrayBuffer,
|
||||
captionSidecar,
|
||||
} satisfies PendingExportSave,
|
||||
};
|
||||
},
|
||||
@@ -4458,6 +4509,10 @@ export default function VideoEditor() {
|
||||
if (result.success && (result.blob || result.tempFilePath)) {
|
||||
const timestamp = Date.now();
|
||||
const fileName = `export-${timestamp}.mp4`;
|
||||
const sidecarForThisExport =
|
||||
settings.includeCaptionSidecar && captionSidecarPayload
|
||||
? captionSidecarPayload
|
||||
: undefined;
|
||||
markExportAsSaving();
|
||||
|
||||
let saveResult: {
|
||||
@@ -4479,8 +4534,13 @@ export default function VideoEditor() {
|
||||
smokeExportConfig.enabled && smokeExportConfig.outputPath
|
||||
? smokeExportConfig.outputPath
|
||||
: null,
|
||||
captionSidecar: sidecarForThisExport,
|
||||
});
|
||||
pendingOnCancel = { fileName, tempFilePath: result.tempFilePath };
|
||||
pendingOnCancel = {
|
||||
fileName,
|
||||
tempFilePath: result.tempFilePath,
|
||||
captionSidecar: sidecarForThisExport,
|
||||
};
|
||||
} else if (result.blob) {
|
||||
// Legacy fallback: some export paths still surface a Blob, but in
|
||||
// Electron we stream it into a temp file first so save/finalize
|
||||
@@ -4489,6 +4549,7 @@ export default function VideoEditor() {
|
||||
result.blob,
|
||||
fileName,
|
||||
smokeExportConfig.enabled ? smokeExportConfig.outputPath : null,
|
||||
sidecarForThisExport,
|
||||
);
|
||||
saveResult = blobSave.saveResult;
|
||||
pendingOnCancel = blobSave.pendingSave;
|
||||
@@ -4701,6 +4762,7 @@ export default function VideoEditor() {
|
||||
annotationRegions,
|
||||
autoCaptions,
|
||||
autoCaptionSettings,
|
||||
captionSidecarPayload,
|
||||
isPlaying,
|
||||
exportQuality,
|
||||
effectiveZoomRegions,
|
||||
@@ -4858,6 +4920,7 @@ export default function VideoEditor() {
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
exportFormat,
|
||||
includeCaptionSidecar: hasCaptionsForSidecar && includeCaptionSidecar,
|
||||
exportEncodingMode,
|
||||
exportQuality,
|
||||
mp4FrameRate,
|
||||
@@ -4881,6 +4944,8 @@ export default function VideoEditor() {
|
||||
gifFrameRate,
|
||||
gifLoop,
|
||||
gifSizePreset,
|
||||
hasCaptionsForSidecar,
|
||||
includeCaptionSidecar,
|
||||
exportBackendPreference,
|
||||
exportPipelineModel,
|
||||
handleExport,
|
||||
@@ -4925,11 +4990,13 @@ export default function VideoEditor() {
|
||||
tempPath: pendingSave.tempFilePath,
|
||||
fileName: pendingSave.fileName,
|
||||
outputPath: null,
|
||||
captionSidecar: pendingSave.captionSidecar,
|
||||
});
|
||||
} else if (pendingSave.arrayBuffer) {
|
||||
saveResult = await window.electronAPI.saveExportedVideo(
|
||||
pendingSave.arrayBuffer,
|
||||
pendingSave.fileName,
|
||||
pendingSave.captionSidecar,
|
||||
);
|
||||
} else {
|
||||
saveResult = { success: false, message: "No pending export to save" };
|
||||
@@ -5669,6 +5736,9 @@ export default function VideoEditor() {
|
||||
onGifLoopChange={setGifLoop}
|
||||
gifSizePreset={gifSizePreset}
|
||||
onGifSizePresetChange={setGifSizePreset}
|
||||
showCaptionSidecarOption={hasCaptionsForSidecar && exportFormat === "mp4"}
|
||||
includeCaptionSidecar={includeCaptionSidecar}
|
||||
onIncludeCaptionSidecarChange={setIncludeCaptionSidecar}
|
||||
mp4OutputDimensions={mp4OutputDimensions}
|
||||
gifOutputDimensions={gifOutputDimensions}
|
||||
onExport={handleStartExportFromDropdown}
|
||||
|
||||
@@ -5,6 +5,7 @@ const baseOptions = {
|
||||
sourceWidth: 1920,
|
||||
sourceHeight: 1080,
|
||||
exportFormat: "mp4" as const,
|
||||
includeCaptionSidecar: true,
|
||||
exportEncodingMode: "balanced" as const,
|
||||
exportQuality: "good" as const,
|
||||
mp4FrameRate: 30 as const,
|
||||
@@ -19,6 +20,7 @@ describe("resolveExportStartSettings", () => {
|
||||
it("preserves MP4 dropdown settings", () => {
|
||||
expect(resolveExportStartSettings(baseOptions)).toEqual({
|
||||
format: "mp4",
|
||||
includeCaptionSidecar: true,
|
||||
encodingMode: "balanced",
|
||||
mp4FrameRate: 30,
|
||||
backendPreference: "auto",
|
||||
@@ -41,6 +43,7 @@ describe("resolveExportStartSettings", () => {
|
||||
}),
|
||||
).toEqual({
|
||||
format: "gif",
|
||||
includeCaptionSidecar: false,
|
||||
encodingMode: undefined,
|
||||
mp4FrameRate: undefined,
|
||||
backendPreference: undefined,
|
||||
|
||||
@@ -16,6 +16,7 @@ export function resolveExportStartSettings({
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
exportFormat,
|
||||
includeCaptionSidecar,
|
||||
exportEncodingMode,
|
||||
exportQuality,
|
||||
mp4FrameRate,
|
||||
@@ -28,6 +29,7 @@ export function resolveExportStartSettings({
|
||||
sourceWidth: number;
|
||||
sourceHeight: number;
|
||||
exportFormat: ExportFormat;
|
||||
includeCaptionSidecar: boolean;
|
||||
exportEncodingMode: ExportEncodingMode;
|
||||
exportQuality: ExportQuality;
|
||||
mp4FrameRate: ExportMp4FrameRate;
|
||||
@@ -44,6 +46,7 @@ export function resolveExportStartSettings({
|
||||
|
||||
return {
|
||||
format: exportFormat,
|
||||
includeCaptionSidecar: exportFormat === "mp4" ? includeCaptionSidecar : false,
|
||||
encodingMode: exportFormat === "mp4" ? exportEncodingMode : undefined,
|
||||
mp4FrameRate: exportFormat === "mp4" ? mp4FrameRate : undefined,
|
||||
backendPreference: exportFormat === "mp4" ? exportBackendPreference : undefined,
|
||||
|
||||
@@ -193,6 +193,7 @@ export interface GifExportConfig {
|
||||
|
||||
export interface ExportSettings {
|
||||
format: ExportFormat;
|
||||
includeCaptionSidecar?: boolean;
|
||||
// MP4 settings
|
||||
quality?: ExportQuality;
|
||||
encodingMode?: ExportEncodingMode;
|
||||
|
||||
Reference in New Issue
Block a user