From bb304c8fd7660561ae2791b309cc502f60fb126b Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Mon, 23 May 2022 22:31:48 -0700 Subject: [PATCH] implement random segments closes #1153 --- README.md | 1 + public/menu.js | 6 ++++++ src/App.jsx | 11 +++++++++-- src/dialogs.jsx | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ea9a11a2..92d71456 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ The main feature is lossless trimming and cutting of video and audio files, whic - View subtitles - Customizable keyboard hotkeys - Black scene detection +- Divide timeline into segments with length L or into N segments or even randomized segments! ## Example lossless use cases diff --git a/public/menu.js b/public/menu.js index 31d20e30..39cf6298 100644 --- a/public/menu.js +++ b/public/menu.js @@ -214,6 +214,12 @@ module.exports = (app, mainWindow, newVersion) => { mainWindow.webContents.send('createFixedDurationSegments'); }, }, + { + label: i18n.t('Create random segments'), + click() { + mainWindow.webContents.send('createRandomSegments'); + }, + }, { label: i18n.t('Invert all segments on timeline'), click() { diff --git a/src/App.jsx b/src/App.jsx index 117fbbf8..da383d1c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -69,7 +69,7 @@ import { } from './util'; import { formatDuration } from './util/duration'; import { adjustRate } from './util/rate-calculator'; -import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, confirmExtractFramesAsImages } from './dialogs'; +import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, confirmExtractFramesAsImages } from './dialogs'; import { openSendReportDialog } from './reporting'; import { fallbackLng } from './i18n'; import { createSegment, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, convertSegmentsToChapters, hasAnySegmentOverlap } from './segments'; @@ -1815,6 +1815,12 @@ const App = memo(() => { if (segments) loadCutSegments(segments); }, [checkFileOpened, duration, loadCutSegments]); + const createRandomSegments = useCallback(async () => { + if (!checkFileOpened() || !isDurationValid(duration)) return; + const segments = await createRandomSegmentsDialog(duration); + if (segments) loadCutSegments(segments); + }, [checkFileOpened, duration, loadCutSegments]); + const askSetStartTimeOffset = useCallback(async () => { const newStartTimeOffset = await promptTimeOffset({ initialValue: startTimeOffset !== undefined ? formatDuration({ seconds: startTimeOffset }) : undefined, @@ -2199,6 +2205,7 @@ const App = memo(() => { shuffleSegments, createNumSegments, createFixedDurationSegments, + createRandomSegments, invertAllSegments, fillSegmentsGaps, fixInvalidDuration: tryFixInvalidDuration, @@ -2211,7 +2218,7 @@ const App = memo(() => { const entries = Object.entries(action); entries.forEach(([key, value]) => electron.ipcRenderer.on(key, value)); return () => entries.forEach(([key, value]) => electron.ipcRenderer.removeListener(key, value)); - }, [apparentCutSegments, askSetStartTimeOffset, checkFileOpened, clearSegments, closeBatch, closeFileWithConfirm, concatCurrentBatch, createFixedDurationSegments, createNumSegments, customOutDir, cutSegments, detectBlackScenes, detectedFps, extractAllStreams, fileFormat, filePath, fillSegmentsGaps, getFrameCount, invertAllSegments, loadCutSegments, loadMedia, openSendReportDialogWithState, reorderSegsByStartTime, setWorking, shiftAllSegmentTimes, shuffleSegments, toggleHelp, toggleSettings, tryFixInvalidDuration, userHtml5ifyCurrentFile, userOpenFiles]); + }, [apparentCutSegments, askSetStartTimeOffset, checkFileOpened, clearSegments, closeBatch, closeFileWithConfirm, concatCurrentBatch, createFixedDurationSegments, createNumSegments, createRandomSegments, customOutDir, cutSegments, detectBlackScenes, detectedFps, extractAllStreams, fileFormat, filePath, fillSegmentsGaps, getFrameCount, invertAllSegments, loadCutSegments, loadMedia, openSendReportDialogWithState, reorderSegsByStartTime, setWorking, shiftAllSegmentTimes, shuffleSegments, toggleHelp, toggleSettings, tryFixInvalidDuration, userHtml5ifyCurrentFile, userOpenFiles]); const showAddStreamSourceDialog = useCallback(async () => { try { diff --git a/src/dialogs.jsx b/src/dialogs.jsx index 034aed6a..720a6f81 100644 --- a/src/dialogs.jsx +++ b/src/dialogs.jsx @@ -239,6 +239,40 @@ async function askForSegmentDuration(fileDuration) { return parseDuration(value); } +// https://github.com/mifi/lossless-cut/issues/1153 +async function askForSegmentsRandomDurationRange() { + function parse(str) { + const match = str.replace(/\s/g, '').match(/^duration([\d.]+)to([\d.]+),gap([-\d.]+)to([-\d.]+)$/i); + if (!match) return undefined; + const values = match.slice(1); + const parsed = values.map((val) => parseFloat(val)); + + const durationMin = parsed[0]; + const durationMax = parsed[1]; + const gapMin = parsed[2]; + const gapMax = parsed[3]; + + if (!(parsed.every((val) => !Number.isNaN(val)) && durationMin <= durationMax && gapMin <= gapMax && durationMin > 0)) return undefined; + return { durationMin, durationMax, gapMin, gapMax }; + } + + const { value } = await Swal.fire({ + input: 'text', + showCancelButton: true, + inputValue: 'Duration 3 to 5, Gap 0 to 2', + text: i18n.t('Divide timeline into segments with randomized durations and gaps between sergments, in a range specified in seconds with the correct format.'), + inputValidator: (v) => { + const parsed = parse(v); + if (!parsed) return i18n.t('Invalid input'); + return undefined; + }, + }); + + if (value == null) return undefined; + + return parse(value); +} + async function askForShiftSegmentsVariant(time) { const { value } = await Swal.fire({ input: 'radio', @@ -376,6 +410,23 @@ export async function createFixedDurationSegments(fileDuration) { return edl; } +export async function createRandomSegments(fileDuration) { + const response = await askForSegmentsRandomDurationRange(); + if (response == null) return undefined; + + const { durationMin, durationMax, gapMin, gapMax } = response; + + const randomInRange = (min, max) => min + Math.random() * (max - min); + + const edl = []; + for (let start = 0; start < fileDuration && edl.length < maxSegments; start += randomInRange(gapMin, gapMax)) { + const end = start + randomInRange(durationMin, durationMax); + edl.push({ start, end }); + start = end; + } + return edl; +} + export async function showCutFailedDialog({ detectedFileFormat }) { const html = (