remember Black/scene/silence detect dialog parameters

#371 #2257
pull/2298/head
Mikael Finstad 2 years ago
parent 241210db47
commit 241210d103
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -1,33 +1,38 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { useState, useCallback, useRef, useEffect, FormEvent } from 'react';
import i18n from 'i18next';
import { FaLink } from 'react-icons/fa';
import Swal, { ReactSwal } from '../swal';
import Button from '../components/Button';
import TextInput from '../components/TextInput';
import { FfmpegDialog, getHint, getLabel } from '../ffmpegParameters';
const { shell } = window.require('electron');
export interface ParameterDialogParameter { value: string, label?: string, hint?: string }
export type ParameterDialogParameters = Record<string, ParameterDialogParameter>;
export type ParameterDialogParameters = Record<string, string>;
const ParametersInput = ({ description, parameters: parametersIn, onChange, onSubmit, docUrl }: {
description?: string | undefined, parameters: ParameterDialogParameters, onChange: (a: ParameterDialogParameters) => void, onSubmit: () => void, docUrl?: string | undefined,
const ParametersInput = ({ description, dialogType, parameters: parametersIn, onChange, onSubmit, docUrl }: {
description?: string | undefined,
dialogType: FfmpegDialog,
parameters: ParameterDialogParameters,
onChange: (a: ParameterDialogParameters) => void,
onSubmit: () => void,
docUrl?: string | undefined,
}) => {
const firstInputRef = useRef<HTMLInputElement>(null);
const [parameters, setParameters] = useState(parametersIn);
const getParameter = (key: string) => parameters[key]?.value;
const getParameter = (key: string) => parameters[key];
const handleChange = (key: string, value: string) => setParameters((existing) => {
const newParameters = { ...existing, [key]: { ...existing[key], value } };
const newParameters = { ...existing, [key]: value };
onChange(newParameters);
return newParameters;
});
const handleSubmit = useCallback((e) => {
const handleSubmit = useCallback((e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit();
}, [onSubmit]);
@ -47,7 +52,7 @@ const ParametersInput = ({ description, parameters: parametersIn, onChange, onSu
const id = `parameter-${key}`;
return (
<div key={key} style={{ marginBottom: '.5em' }}>
<label htmlFor={id} style={{ display: 'block', fontFamily: 'monospace', marginBottom: '.3em' }}>{parameter.label || key}</label>
<label htmlFor={id} style={{ display: 'block', fontFamily: 'monospace', marginBottom: '.3em' }}>{getLabel(dialogType, parameter) || key}</label>
<TextInput
id={id}
ref={i === 0 ? firstInputRef : undefined}
@ -55,7 +60,7 @@ const ParametersInput = ({ description, parameters: parametersIn, onChange, onSu
onChange={(e) => handleChange(key, e.target.value)}
style={{ marginBottom: '.2em' }}
/>
{parameter.hint && <div style={{ opacity: 0.6, fontSize: '0.8em' }}>{parameter.hint}</div>}
{getHint(dialogType, key) && <div style={{ opacity: 0.6, fontSize: '0.8em' }}>{getHint(dialogType, key)}</div>}
</div>
);
})}
@ -66,8 +71,12 @@ const ParametersInput = ({ description, parameters: parametersIn, onChange, onSu
);
};
export async function showParametersDialog({ title, description, parameters: parametersIn, docUrl }: {
title?: string, description?: string, parameters: ParameterDialogParameters, docUrl?: string,
export async function showParametersDialog({ title, description, dialogType, parameters: parametersIn, docUrl }: {
title?: string,
description?: string,
dialogType: FfmpegDialog,
parameters: ParameterDialogParameters,
docUrl?: string,
}) {
let parameters = parametersIn;
let resolve1: (value: boolean) => void;
@ -86,6 +95,7 @@ export async function showParametersDialog({ title, description, parameters: par
html: (
<ParametersInput
description={description}
dialogType={dialogType}
parameters={parameters}
onChange={(newParameters) => { parameters = newParameters; }}
onSubmit={handleSubmit}
@ -102,5 +112,5 @@ export async function showParametersDialog({ title, description, parameters: par
const isConfirmed = await Promise.race([promise1, promise2]);
if (!isConfirmed) return undefined;
return Object.fromEntries(Object.entries(parameters).map(([key, parameter]) => [key, parameter.value]));
return parameters;
}

@ -1,43 +0,0 @@
// eslint-disable-line unicorn/filename-case
import i18n from 'i18next';
export const blackdetect = () => ({
black_min_duration: {
value: '2.0',
hint: i18n.t('Set the minimum detected black duration expressed in seconds. It must be a non-negative floating point number.'),
},
picture_black_ratio_th: {
value: '0.98',
hint: i18n.t('Set the threshold for considering a picture "black".'),
},
pixel_black_th: {
value: '0.10',
hint: i18n.t('Set the threshold for considering a pixel "black".'),
},
mode: {
value: '1',
hint: i18n.t('Segment mode: "{{mode1}}" will create segments bounding the black sections. "{{mode2}}" will create segments that start/stop at the center of each black section.', { mode1: '1', mode2: '2' }),
},
});
export const silencedetect = () => ({
noise: {
value: '-60dB',
hint: i18n.t('Set noise tolerance. Can be specified in dB (in case "dB" is appended to the specified value) or amplitude ratio. Default is -60dB, or 0.001.'),
},
duration: {
value: '2.0',
hint: i18n.t('Set minimum silence duration that will be converted into a segment.'),
},
mode: {
value: '1',
hint: i18n.t('Segment mode: "{{mode1}}" will create segments bounding the silent sections. "{{mode2}}" will create segments that start/stop at the center of each silent section.', { mode1: '1', mode2: '2' }),
},
});
export const sceneChange = () => ({
minChange: {
value: '0.3',
hint: i18n.t('Minimum change between two frames to be considered a new scene. A value between 0.3 and 0.5 is generally a sane choice.'),
},
});

@ -0,0 +1,51 @@
// eslint-disable-line unicorn/filename-case
import i18n from 'i18next';
const parametersRaw = {
blackdetect: {
black_min_duration: {
value: '2.0',
hint: () => i18n.t('Set the minimum detected black duration expressed in seconds. It must be a non-negative floating point number.'),
},
picture_black_ratio_th: {
value: '0.98',
hint: () => i18n.t('Set the threshold for considering a picture "black".'),
},
pixel_black_th: {
value: '0.10',
hint: () => i18n.t('Set the threshold for considering a pixel "black".'),
},
mode: {
value: '1',
hint: () => i18n.t('Segment mode: "{{mode1}}" will create segments bounding the black sections. "{{mode2}}" will create segments that start/stop at the center of each black section.', { mode1: '1', mode2: '2' }),
},
},
silencedetect: {
noise: {
value: '-60dB',
hint: () => i18n.t('Set noise tolerance. Can be specified in dB (in case "dB" is appended to the specified value) or amplitude ratio. Default is -60dB, or 0.001.'),
},
duration: {
value: '2.0',
hint: () => i18n.t('Set minimum silence duration that will be converted into a segment.'),
},
mode: {
value: '1',
hint: () => i18n.t('Segment mode: "{{mode1}}" will create segments bounding the silent sections. "{{mode2}}" will create segments that start/stop at the center of each silent section.', { mode1: '1', mode2: '2' }),
},
},
sceneChange: {
minChange: {
value: '0.3',
hint: () => i18n.t('Minimum change between two frames to be considered a new scene. A value between 0.3 and 0.5 is generally a sane choice.'),
},
},
};
export type FfmpegDialog = keyof typeof parametersRaw;
// widen types
export const parameters: Record<FfmpegDialog, Record<string, { value: string, hint?: () => string, label?: string }>> = parametersRaw;
export const getHint = (dialogType: FfmpegDialog, param: string) => parameters[dialogType][param]?.hint?.();
export const getLabel = (dialogType: FfmpegDialog, param: string) => parameters[dialogType][param]?.label;

@ -11,7 +11,7 @@ import { errorToast } from '../swal';
import { showParametersDialog } from '../dialogs/parameters';
import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByExprDialog } from '../dialogs';
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, addSegmentColorIndex } from '../segments';
import * as ffmpegParameters from '../ffmpeg-parameters';
import { parameters as allFfmpegParameters, FfmpegDialog } from '../ffmpegParameters';
import { maxSegmentsAllowed } from '../util/constants';
import { ApparentCutSegment, ParseTimecode, SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
import safeishEval from '../worker/eval';
@ -56,6 +56,19 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
const [currentSegIndex, setCurrentSegIndex] = useState(0);
const [deselectedSegmentIds, setDeselectedSegmentIds] = useState<Record<string, boolean>>({});
const [ffmpegParameters, setFfmpegParameters] = useState(() => Object.fromEntries(Object.entries(allFfmpegParameters).map(([dialogType, parameters]) => ([
dialogType,
Object.fromEntries(Object.entries(parameters).map(([k2, v2]) => [k2, v2.value])),
] as const))));
const setFfmpegParametersForDialog = useCallback((dialogType: FfmpegDialog, newParams: Record<string, string>) => setFfmpegParameters((existing) => ({
...existing,
[dialogType]: {
...existing[dialogType],
...newParams,
},
})), []);
const isSegmentSelected = useCallback(({ segId }: { segId: string }) => !deselectedSegmentIds[segId], [deselectedSegmentIds]);
@ -148,33 +161,45 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
const selectedSegmentsRaw = useMemo(() => apparentCutSegments.filter((segment) => isSegmentSelected(segment)), [apparentCutSegments, isSegmentSelected]);
const getFfmpegParameters = useCallback((key: FfmpegDialog) => {
const parameters = ffmpegParameters[key];
invariant(parameters);
return parameters;
}, [ffmpegParameters]);
const detectBlackScenes = useCallback(async () => {
const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.blackdetect(), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#blackdetect' });
const dialogType = 'blackdetect';
const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#blackdetect' });
if (parameters == null) return;
const { mode, ...filterOptions } = parameters;
setFfmpegParametersForDialog(dialogType, parameters);
invariant(mode === '1' || mode === '2');
invariant(filePath != null);
await detectSegments({ name: 'blackScenes', workingText: i18n.t('Detecting black scenes'), errorText: i18n.t('Failed to detect black scenes'), fn: async () => blackDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end }) });
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, setProgress]);
}, [getFfmpegParameters, setFfmpegParametersForDialog, filePath, detectSegments, setProgress, currentApparentCutSeg.start, currentApparentCutSeg.end]);
const detectSilentScenes = useCallback(async () => {
const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.silencedetect(), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#silencedetect' });
const dialogType = 'silencedetect';
const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType), docUrl: 'https://ffmpeg.org/ffmpeg-filters.html#silencedetect' });
if (parameters == null) return;
setFfmpegParametersForDialog(dialogType, parameters);
const { mode, ...filterOptions } = parameters;
invariant(mode === '1' || mode === '2');
invariant(filePath != null);
await detectSegments({ name: 'silentScenes', workingText: i18n.t('Detecting silent scenes'), errorText: i18n.t('Failed to detect silent scenes'), fn: async () => silenceDetect({ filePath, filterOptions, boundingMode: mode === '1', onProgress: setProgress, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end }) });
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, setProgress]);
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]);
const detectSceneChanges = useCallback(async () => {
const filterOptions = await showParametersDialog({ title: i18n.t('Enter parameters'), parameters: ffmpegParameters.sceneChange() });
if (filterOptions == null) return;
const dialogType = 'sceneChange';
const parameters = await showParametersDialog({ title: i18n.t('Enter parameters'), dialogType, parameters: getFfmpegParameters(dialogType) });
if (parameters == null) return;
setFfmpegParametersForDialog(dialogType, parameters);
invariant(filePath != null);
// eslint-disable-next-line prefer-destructuring
const minChange = filterOptions['minChange'];
const minChange = parameters['minChange'];
invariant(minChange != null);
await detectSegments({ name: 'sceneChanges', workingText: i18n.t('Detecting scene changes'), errorText: i18n.t('Failed to detect scene changes'), fn: async () => ffmpegDetectSceneChanges({ filePath, minChange, onProgress: setProgress, from: currentApparentCutSeg.start, to: currentApparentCutSeg.end }) });
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, setProgress]);
}, [currentApparentCutSeg.end, currentApparentCutSeg.start, detectSegments, filePath, getFfmpegParameters, setFfmpegParametersForDialog, setProgress]);
const createSegmentsFromKeyframes = useCallback(async () => {
if (!videoStream) return;

Loading…
Cancel
Save