1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 11:43:17 +01:00

select segments by tag

This commit is contained in:
Mikael Finstad 2023-09-06 12:52:46 +02:00
parent ecc852a2bc
commit 1acf72fcfd
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
5 changed files with 49 additions and 11 deletions

View File

@ -83,6 +83,7 @@ The main feature is lossless trimming and cutting of video and audio files, whic
- Losslessly split a video into one file per scene (note you probably have to shift segments, see [#330](https://github.com/mifi/lossless-cut/issues/330).) - Losslessly split a video into one file per scene (note you probably have to shift segments, see [#330](https://github.com/mifi/lossless-cut/issues/330).)
- Cut away silent parts of an audio/video - Cut away silent parts of an audio/video
- Split video into segments to for example respect Twitter's 140 second limit - Split video into segments to for example respect Twitter's 140 second limit
- Annotate each segment with one or more tags, then use those tags to organize your segments or use it to create an output folder structure or hierarchy for your segments.
### Export cut times as YouTube Chapters ### Export cut times as YouTube Chapters
1. Export with Merge and "Create chapters from merged segments" enabled 1. Export with Merge and "Create chapters from merged segments" enabled

View File

@ -355,7 +355,7 @@ const App = memo(() => {
}, [isFileOpened]); }, [isFileOpened]);
const { const {
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, onViewSegmentTags, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, onViewSegmentTags, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByTag, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment,
} = useSegments({ filePath, workingRef, setWorking, setCutProgress, mainVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly }); } = useSegments({ filePath, workingRef, setWorking, setCutProgress, mainVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly });
const jumpSegStart = useCallback((index) => userSeekAbs(apparentCutSegments[index].start), [apparentCutSegments, userSeekAbs]); const jumpSegStart = useCallback((index) => userSeekAbs(apparentCutSegments[index].start), [apparentCutSegments, userSeekAbs]);
@ -2386,6 +2386,7 @@ const App = memo(() => {
jumpSegEnd={jumpSegEnd} jumpSegEnd={jumpSegEnd}
onViewSegmentTags={onViewSegmentTags} onViewSegmentTags={onViewSegmentTags}
onSelectSegmentsByLabel={onSelectSegmentsByLabel} onSelectSegmentsByLabel={onSelectSegmentsByLabel}
onSelectSegmentsByTag={onSelectSegmentsByTag}
onLabelSelectedSegments={onLabelSelectedSegments} onLabelSelectedSegments={onLabelSelectedSegments}
/> />
)} )}

View File

@ -23,7 +23,7 @@ const buttonBaseStyle = {
const neutralButtonColor = 'var(--gray8)'; const neutralButtonColor = 'var(--gray8)';
const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onViewSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => { const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onViewSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { getSegColor } = useSegColors(); const { getSegColor } = useSegColors();
@ -48,6 +48,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
{ label: t('Select all segments'), click: () => onSelectAllSegments() }, { label: t('Select all segments'), click: () => onSelectAllSegments() },
{ label: t('Deselect all segments'), click: () => onDeselectAllSegments() }, { label: t('Deselect all segments'), click: () => onDeselectAllSegments() },
{ label: t('Select segments by label'), click: () => onSelectSegmentsByLabel(seg) }, { label: t('Select segments by label'), click: () => onSelectSegmentsByLabel(seg) },
{ label: t('Select segments by tag'), click: () => onSelectSegmentsByTag(seg) },
{ label: t('Invert selected segments'), click: () => onInvertSelectedSegments() }, { label: t('Invert selected segments'), click: () => onInvertSelectedSegments() },
{ type: 'separator' }, { type: 'separator' },
@ -66,7 +67,7 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
{ label: t('Segment tags'), click: () => onViewSegmentTags(index) }, { label: t('Segment tags'), click: () => onViewSegmentTags(index) },
{ label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg.segId]) }, { label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg.segId]) },
]; ];
}, [invertCutSegments, t, jumpSegStart, jumpSegEnd, addSegment, onLabelPress, onRemovePress, onLabelSelectedSegments, onRemoveSelected, onReorderPress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onInvertSelectedSegments, updateOrder, onViewSegmentTags, index, onExtractSegmentFramesAsImages]); }, [invertCutSegments, t, jumpSegStart, jumpSegEnd, addSegment, onLabelPress, onRemovePress, onLabelSelectedSegments, onRemoveSelected, onReorderPress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, updateOrder, onViewSegmentTags, index, onExtractSegmentFramesAsImages]);
useContextMenu(ref, contextMenuTemplate); useContextMenu(ref, contextMenuTemplate);
@ -157,7 +158,7 @@ const SegmentList = memo(({
currentSegIndex, currentSegIndex,
updateSegOrder, updateSegOrders, addSegment, removeCutSegment, onRemoveSelected, updateSegOrder, updateSegOrders, addSegment, removeCutSegment, onRemoveSelected,
onLabelSegment, currentCutSeg, segmentAtCursor, toggleSegmentsList, splitCurrentSegment, onLabelSegment, currentCutSeg, segmentAtCursor, toggleSegmentsList, splitCurrentSegment,
selectedSegments, isSegmentSelected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectAllSegments, onSelectSegmentsByLabel, onExtractSegmentFramesAsImages, onLabelSelectedSegments, onInvertSelectedSegments, onDuplicateSegmentClick, selectedSegments, isSegmentSelected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onExtractSegmentFramesAsImages, onLabelSelectedSegments, onInvertSelectedSegments, onDuplicateSegmentClick,
jumpSegStart, jumpSegEnd, onViewSegmentTags, jumpSegStart, jumpSegEnd, onViewSegmentTags,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -318,6 +319,7 @@ const SegmentList = memo(({
onSelectAllSegments={onSelectAllSegments} onSelectAllSegments={onSelectAllSegments}
onViewSegmentTags={onViewSegmentTags} onViewSegmentTags={onViewSegmentTags}
onSelectSegmentsByLabel={onSelectSegmentsByLabel} onSelectSegmentsByLabel={onSelectSegmentsByLabel}
onSelectSegmentsByTag={onSelectSegmentsByTag}
onExtractSegmentFramesAsImages={onExtractSegmentFramesAsImages} onExtractSegmentFramesAsImages={onExtractSegmentFramesAsImages}
onLabelSelectedSegments={onLabelSelectedSegments} onLabelSelectedSegments={onLabelSelectedSegments}
onInvertSelectedSegments={onInvertSelectedSegments} onInvertSelectedSegments={onInvertSelectedSegments}

View File

@ -505,6 +505,26 @@ export async function selectSegmentsByLabelDialog(currentName) {
return value; return value;
} }
export async function selectSegmentsByTagDialog() {
const { value: value1 } = await Swal.fire({
showCancelButton: true,
title: i18n.t('Select segments by tag'),
text: i18n.t('Enter tag name (in the next dialog you\'ll enter tag value)'),
input: 'text',
});
if (!value1) return undefined;
const { value: value2 } = await Swal.fire({
showCancelButton: true,
title: i18n.t('Select segments by tag'),
text: i18n.t('Enter tag value'),
input: 'text',
});
if (!value2) return undefined;
return { tagName: value1, tagValue: value2 };
}
export async function showEditableJsonDialog({ text, title, inputLabel, inputValue, inputValidator }) { export async function showEditableJsonDialog({ text, title, inputLabel, inputValue, inputValidator }) {
const { value } = await Swal.fire({ const { value } = await Swal.fire({
input: 'textarea', input: 'textarea',

View File

@ -10,7 +10,7 @@ import { blackDetect, silenceDetect, detectSceneChanges as ffmpegDetectSceneChan
import { handleError, shuffleArray } from '../util'; import { handleError, shuffleArray } from '../util';
import { errorToast } from '../swal'; import { errorToast } from '../swal';
import { showParametersDialog } from '../dialogs/parameters'; import { showParametersDialog } from '../dialogs/parameters';
import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, showEditableJsonDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog } from '../dialogs'; import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, showEditableJsonDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByTagDialog } from '../dialogs';
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2 } from '../segments'; import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2 } from '../segments';
import * as ffmpegParameters from '../ffmpeg-parameters'; import * as ffmpegParameters from '../ffmpeg-parameters';
import { maxSegmentsAllowed } from '../util/constants'; import { maxSegmentsAllowed } from '../util/constants';
@ -436,18 +436,31 @@ export default ({
if (segments) loadCutSegments(segments); if (segments) loadCutSegments(segments);
}, [checkFileOpened, duration, loadCutSegments]); }, [checkFileOpened, duration, loadCutSegments]);
const onSelectSegmentsByLabel = useCallback(async () => { const enableSegments = useCallback((segmentsToEnable) => {
const { name } = currentCutSeg;
const value = await selectSegmentsByLabelDialog(name);
if (value == null) return;
const segmentsToEnable = cutSegments.filter((seg) => (seg.name || '') === value);
if (segmentsToEnable.length === 0 || segmentsToEnable.length === cutSegments.length) return; // no point if (segmentsToEnable.length === 0 || segmentsToEnable.length === cutSegments.length) return; // no point
setDeselectedSegmentIds((existing) => { setDeselectedSegmentIds((existing) => {
const ret = { ...existing }; const ret = { ...existing };
segmentsToEnable.forEach(({ segId }) => { ret[segId] = false; }); segmentsToEnable.forEach(({ segId }) => { ret[segId] = false; });
return ret; return ret;
}); });
}, [currentCutSeg, cutSegments]); }, [cutSegments.length]);
const onSelectSegmentsByLabel = useCallback(async () => {
const { name } = currentCutSeg;
const value = await selectSegmentsByLabelDialog(name);
if (value == null) return;
const segmentsToEnable = cutSegments.filter((seg) => (seg.name || '') === value);
enableSegments(segmentsToEnable);
}, [currentCutSeg, cutSegments, enableSegments]);
const onSelectSegmentsByTag = useCallback(async () => {
const value = await selectSegmentsByTagDialog();
if (value == null) return;
const { tagName, tagValue } = value;
const segmentsToEnable = cutSegments.filter((seg) => getSegmentTags(seg)[tagName] === tagValue);
enableSegments(segmentsToEnable);
}, [cutSegments, enableSegments]);
const onLabelSelectedSegments = useCallback(async () => { const onLabelSelectedSegments = useCallback(async () => {
if (selectedSegmentsRaw.length < 1) return; if (selectedSegmentsRaw.length < 1) return;
@ -540,6 +553,7 @@ export default ({
invertSelectedSegments, invertSelectedSegments,
removeSelectedSegments, removeSelectedSegments,
onSelectSegmentsByLabel, onSelectSegmentsByLabel,
onSelectSegmentsByTag,
toggleSegmentSelected, toggleSegmentSelected,
selectOnlySegment, selectOnlySegment,
setCutTime, setCutTime,