1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-21 18:02:35 +01:00

allow selecting segments by expression

also remove select by tags dialog to reduce code
it's covered by expression

fixes #1999
This commit is contained in:
Mikael Finstad 2024-05-16 14:10:00 +02:00
parent 58adc59c9d
commit b5028dc142
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
6 changed files with 181 additions and 42 deletions

View File

@ -131,6 +131,7 @@
"i18next-fs-backend": "^2.1.1",
"json5": "^2.2.2",
"lodash": "^4.17.19",
"mathjs": "^12.4.2",
"mime-types": "^2.1.14",
"morgan": "^1.10.0",
"semver": "^7.6.0",

View File

@ -406,7 +406,7 @@ function App() {
}, [detectedFps, timecodeFormat, getFrameCount]);
const {
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, 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, updateSegAtIndex,
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, 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, onSelectSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex,
} = useSegments({ filePath, workingRef, setWorking, setCutProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode });
@ -2637,7 +2637,7 @@ function App() {
jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd}
onSelectSegmentsByLabel={onSelectSegmentsByLabel}
onSelectSegmentsByTag={onSelectSegmentsByTag}
onSelectSegmentsByExpr={onSelectSegmentsByExpr}
onLabelSelectedSegments={onLabelSelectedSegments}
updateSegAtIndex={updateSegAtIndex}
editingSegmentTags={editingSegmentTags}

View File

@ -44,7 +44,7 @@ const Segment = memo(({
onToggleSegmentSelected,
onDeselectAllSegments,
onSelectSegmentsByLabel,
onSelectSegmentsByTag,
onSelectSegmentsByExpr,
onSelectAllSegments,
jumpSegStart,
jumpSegEnd,
@ -71,7 +71,7 @@ const Segment = memo(({
onToggleSegmentSelected: UseSegments['toggleSegmentSelected'],
onDeselectAllSegments: UseSegments['deselectAllSegments'],
onSelectSegmentsByLabel: UseSegments['onSelectSegmentsByLabel'],
onSelectSegmentsByTag: UseSegments['onSelectSegmentsByTag'],
onSelectSegmentsByExpr: UseSegments['onSelectSegmentsByExpr'],
onSelectAllSegments: UseSegments['selectAllSegments'],
jumpSegStart: (i: number) => void,
jumpSegEnd: (i: number) => void,
@ -109,7 +109,7 @@ const Segment = memo(({
{ label: t('Select all segments'), click: () => onSelectAllSegments() },
{ label: t('Deselect all segments'), click: () => onDeselectAllSegments() },
{ label: t('Select segments by label'), click: () => onSelectSegmentsByLabel() },
{ label: t('Select segments by tag'), click: () => onSelectSegmentsByTag() },
{ label: t('Select segments by expression'), click: () => onSelectSegmentsByExpr() },
{ label: t('Invert selected segments'), click: () => onInvertSelectedSegments() },
{ type: 'separator' },
@ -128,7 +128,7 @@ const Segment = memo(({
{ label: t('Segment tags'), click: () => onEditSegmentTags(index) },
{ label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg.segId]) },
];
}, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]);
}, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onInvertSelectedSegments, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]);
useContextMenu(ref, contextMenuTemplate);
@ -243,7 +243,7 @@ const SegmentList = memo(({
onDeselectAllSegments,
onSelectAllSegments,
onSelectSegmentsByLabel,
onSelectSegmentsByTag,
onSelectSegmentsByExpr,
onExtractSegmentFramesAsImages,
onLabelSelectedSegments,
onInvertSelectedSegments,
@ -281,7 +281,7 @@ const SegmentList = memo(({
onDeselectAllSegments: UseSegments['deselectAllSegments'],
onSelectAllSegments: UseSegments['selectAllSegments'],
onSelectSegmentsByLabel: UseSegments['onSelectSegmentsByLabel'],
onSelectSegmentsByTag: UseSegments['onSelectSegmentsByTag'],
onSelectSegmentsByExpr: UseSegments['onSelectSegmentsByExpr'],
onExtractSegmentFramesAsImages: (segIds: string[]) => Promise<void>,
onLabelSelectedSegments: UseSegments['onLabelSelectedSegments'],
onInvertSelectedSegments: UseSegments['invertSelectedSegments'],
@ -487,7 +487,7 @@ const SegmentList = memo(({
onSelectAllSegments={onSelectAllSegments}
onEditSegmentTags={onEditSegmentTags}
onSelectSegmentsByLabel={onSelectSegmentsByLabel}
onSelectSegmentsByTag={onSelectSegmentsByTag}
onSelectSegmentsByExpr={onSelectSegmentsByExpr}
onExtractSegmentFramesAsImages={onExtractSegmentFramesAsImages}
onLabelSelectedSegments={onLabelSelectedSegments}
onInvertSelectedSegments={onInvertSelectedSegments}

View File

@ -434,7 +434,7 @@ export async function createFixedDurationSegments({ fileDuration, inputPlacehold
return edl;
}
export async function createRandomSegments(fileDuration) {
export async function createRandomSegments(fileDuration: number) {
const response = await askForSegmentsRandomDurationRange();
if (response == null) return undefined;
@ -451,14 +451,14 @@ export async function createRandomSegments(fileDuration) {
return edl;
}
const MovSuggestion = ({ fileFormat }) => (fileFormat === 'mp4' ? <li><Trans>Change output <b>Format</b> from <b>MP4</b> to <b>MOV</b></Trans></li> : null);
const MovSuggestion = ({ fileFormat }: { fileFormat: string | undefined }) => (fileFormat === 'mp4' ? <li><Trans>Change output <b>Format</b> from <b>MP4</b> to <b>MOV</b></Trans></li> : null);
const OutputFormatSuggestion = () => <li><Trans>Select a different output <b>Format</b> (<b>matroska</b> and <b>mp4</b> support most codecs)</Trans></li>;
const WorkingDirectorySuggestion = () => <li><Trans>Set a different <b>Working directory</b></Trans></li>;
const DifferentFileSuggestion = () => <li><Trans>Try with a <b>Different file</b></Trans></li>;
const HelpSuggestion = () => <li><Trans>See <b>Help</b></Trans> menu</li>;
const ErrorReportSuggestion = () => <li><Trans>If nothing helps, you can send an <b>Error report</b></Trans></li>;
export async function showExportFailedDialog({ fileFormat, safeOutputFileName }) {
export async function showExportFailedDialog({ fileFormat, safeOutputFileName }: { fileFormat: string | undefined, safeOutputFileName: boolean }) {
const html = (
<div style={{ textAlign: 'left' }}>
<Trans>Try one of the following before exporting again:</Trans>
@ -480,7 +480,7 @@ export async function showExportFailedDialog({ fileFormat, safeOutputFileName })
return value;
}
export async function showConcatFailedDialog({ fileFormat }) {
export async function showConcatFailedDialog({ fileFormat }: { fileFormat: string | undefined }) {
const html = (
<div style={{ textAlign: 'left' }}>
<Trans>Try each of the following before merging again:</Trans>
@ -517,7 +517,7 @@ export function openYouTubeChaptersDialog(text: string) {
});
}
export async function labelSegmentDialog({ currentName, maxLength }) {
export async function labelSegmentDialog({ currentName, maxLength }: { currentName: string, maxLength: number }) {
const { value } = await Swal.fire({
showCancelButton: true,
title: i18n.t('Label current segment'),
@ -528,7 +528,7 @@ export async function labelSegmentDialog({ currentName, maxLength }) {
return value;
}
export async function selectSegmentsByLabelDialog(currentName) {
export async function selectSegmentsByLabelDialog(currentName: string) {
const { value } = await Swal.fire({
showCancelButton: true,
title: i18n.t('Select segments by label'),
@ -538,24 +538,47 @@ export async function selectSegmentsByLabelDialog(currentName) {
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;
export async function selectSegmentsByExprDialog(inputValidator: (v: string) => string | undefined) {
const examples = {
duration: { name: i18n.t('Segment duration less than 5 seconds'), code: 'segment.duration < 5' },
start: { name: i18n.t('Segment starts after 00:60'), code: 'segment.start > 60' },
label: { name: i18n.t('Segment label'), code: "equalText(segment.label, 'My label')" },
tag: { name: i18n.t('Segment tag value'), code: "equalText(segment.tags.myTag, 'tag value')" },
};
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;
function addExample(type: string) {
Swal.getInput()!.value = examples[type]?.code ?? '';
}
return { tagName: value1, tagValue: value2 };
const { value } = await ReactSwal.fire<string>({
showCancelButton: true,
title: i18n.t('Select segments by expression'),
input: 'text',
html: (
<div style={{ textAlign: 'left' }}>
<div style={{ marginBottom: '1em' }}>
{i18n.t('Enter an expression which will be evaluated for each segment. Segments for which the expression evaluates to "true" will be selected. For available syntax, see {{url}}.', { url: 'https://mathjs.org/' })}
</div>
<div><b>{i18n.t('Variables')}:</b></div>
<div style={{ marginBottom: '1em' }}>
segment.label, segment.start, segment.end, segment.duration
</div>
<div><b>{i18n.t('Examples')}:</b></div>
{Object.entries(examples).map(([key, { name }]) => (
<button key={key} type="button" onClick={() => addExample(key)} className="button-unstyled" style={{ display: 'block', marginBottom: '.1em' }}>
{name}
</button>
))}
</div>
),
inputPlaceholder: 'segment.duration < 5',
inputValidator,
});
return value;
}
export function showJson5Dialog({ title, json }: { title: string, json: unknown }) {
@ -631,7 +654,7 @@ export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }) {
const fps = detectedFps || 1;
const currentFps = fps * outputPlaybackRate;
function parseValue(v) {
function parseValue(v: string) {
const newFps = parseFloat(v);
if (!Number.isNaN(newFps)) {
return newFps / fps;

View File

@ -3,15 +3,15 @@ import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
import i18n from 'i18next';
import pMap from 'p-map';
import invariant from 'tiny-invariant';
import { evaluate } from 'mathjs';
import sortBy from 'lodash/sortBy';
import { detectSceneChanges as ffmpegDetectSceneChanges, readFrames, mapTimesToSegments, findKeyframeNearTime } from '../ffmpeg';
import { handleError, shuffleArray } from '../util';
import { errorToast } from '../swal';
import { showParametersDialog } from '../dialogs/parameters';
import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByTagDialog } from '../dialogs';
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, addSegmentColorIndex } from '../segments';
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 { maxSegmentsAllowed } from '../util/constants';
import { ParseTimecode, SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types';
@ -454,7 +454,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
if (segments) loadCutSegments(segments);
}, [checkFileOpened, duration, loadCutSegments]);
const enableSegments = useCallback((segmentsToEnable) => {
const enableSegments = useCallback((segmentsToEnable: { segId: string }[]) => {
if (segmentsToEnable.length === 0 || segmentsToEnable.length === cutSegments.length) return; // no point
setDeselectedSegmentIds((existing) => {
const ret = { ...existing };
@ -471,13 +471,43 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
enableSegments(segmentsToEnable);
}, [currentCutSeg, cutSegments, enableSegments]);
const onSelectSegmentsByTag = useCallback(async () => {
const value = await selectSegmentsByTagDialog();
const onSelectSegmentsByExpr = useCallback(async () => {
function matchSegment(seg: StateSegment, expr: string) {
const start = getSegApparentStart(seg);
const end = getSegApparentEnd(seg);
// must clone tags because scope is mutable (editable by expression)
const scopeSegment: { label: string, start: number, end: number, duration: number, tags: Record<string, string> } = { label: seg.name, start, end, duration: end - start, tags: { ...seg.tags } };
return evaluate(expr, { segment: scopeSegment }) === true;
}
const getSegmentsToEnable = (expr: string) => cutSegments.filter((seg) => {
try {
return matchSegment(seg, expr);
} catch (err) {
if (err instanceof TypeError) {
return false;
}
throw err;
}
});
const value = await selectSegmentsByExprDialog((v: string) => {
try {
const segments = getSegmentsToEnable(v);
if (segments.length === 0) return i18n.t('No segments matched');
return undefined;
} catch (err) {
if (err instanceof Error) {
return err.message;
}
throw err;
}
});
if (value == null) return;
const { tagName, tagValue } = value;
const segmentsToEnable = cutSegments.filter((seg) => getSegmentTags(seg)[tagName] === tagValue);
const segmentsToEnable = getSegmentsToEnable(value);
enableSegments(segmentsToEnable);
}, [cutSegments, enableSegments]);
}, [cutSegments, enableSegments, getSegApparentEnd]);
const onLabelSelectedSegments = useCallback(async () => {
if (selectedSegmentsRaw.length === 0) return;
@ -570,7 +600,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt
invertSelectedSegments,
removeSelectedSegments,
onSelectSegmentsByLabel,
onSelectSegmentsByTag,
onSelectSegmentsByExpr,
toggleSegmentSelected,
selectOnlySegment,
setCutTime,

View File

@ -545,6 +545,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.24.4":
version: 7.24.5
resolution: "@babel/runtime@npm:7.24.5"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: e0f4f4d4503f7338749d1dd92361ad132d683bde64e6b61d6c855e100dcd01592295fcfdcc960c946b85ef7908dc2f501080da58447c05812cf3cd80c599bb62
languageName: node
linkType: hard
"@babel/template@npm:^7.20.7":
version: 7.20.7
resolution: "@babel/template@npm:7.20.7"
@ -3898,6 +3907,13 @@ __metadata:
languageName: node
linkType: hard
"complex.js@npm:^2.1.1":
version: 2.1.1
resolution: "complex.js@npm:2.1.1"
checksum: 1905d5204dd8a4d6f591182aca2045986f1ff3c5373e455ccd10c6ee2905bf1d3811a313d38c68f8a8507523202f91e25177387e3adc386c1b5b5ec2f13a6dbb
languageName: node
linkType: hard
"compute-scroll-into-view@npm:^1.0.14, compute-scroll-into-view@npm:^1.0.17":
version: 1.0.17
resolution: "compute-scroll-into-view@npm:1.0.17"
@ -4238,6 +4254,13 @@ __metadata:
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3":
version: 10.4.3
resolution: "decimal.js@npm:10.4.3"
checksum: de663a7bc4d368e3877db95fcd5c87b965569b58d16cdc4258c063d231ca7118748738df17cd638f7e9dd0be8e34cec08d7234b20f1f2a756a52fc5a38b188d0
languageName: node
linkType: hard
"decompress-response@npm:^6.0.0":
version: 6.0.0
resolution: "decompress-response@npm:6.0.0"
@ -5195,6 +5218,13 @@ __metadata:
languageName: node
linkType: hard
"escape-latex@npm:^1.2.0":
version: 1.2.0
resolution: "escape-latex@npm:1.2.0"
checksum: 73a787319f0965ecb8244bb38bf3a3cba872f0b9a5d3da8821140e9f39fe977045dc953a62b1a2bed4d12bfccbe75a7d8ec786412bf00739eaa2f627d0a8e0d6
languageName: node
linkType: hard
"escape-string-regexp@npm:5.0.0":
version: 5.0.0
resolution: "escape-string-regexp@npm:5.0.0"
@ -5945,6 +5975,13 @@ __metadata:
languageName: node
linkType: hard
"fraction.js@npm:4.3.4":
version: 4.3.4
resolution: "fraction.js@npm:4.3.4"
checksum: 3a1e6b268038ffdea625fab6a8d155d7ab644d35d0c99bc59084bfd29fbc714f3a38381b0627751ddb5f188bcde0b3f48c27e80eeb2ecd440825a7d2cd2bf9f1
languageName: node
linkType: hard
"framer-motion@npm:^9.0.3":
version: 9.0.3
resolution: "framer-motion@npm:9.0.3"
@ -7498,6 +7535,13 @@ __metadata:
languageName: node
linkType: hard
"javascript-natural-sort@npm:^0.7.1":
version: 0.7.1
resolution: "javascript-natural-sort@npm:0.7.1"
checksum: 7bf6eab67871865d347f09a95aa770f9206c1ab0226bcda6fdd9edec340bf41111a7f82abac30556aa16a21cfa3b2b1ca4a362c8b73dd5ce15220e5d31f49d79
languageName: node
linkType: hard
"js-cookie@npm:^2.2.1":
version: 2.2.1
resolution: "js-cookie@npm:2.2.1"
@ -7949,6 +7993,7 @@ __metadata:
ky: "npm:^0.33.1"
lodash: "npm:^4.17.19"
luxon: "npm:^3.3.0"
mathjs: "npm:^12.4.2"
mime-types: "npm:^2.1.14"
mkdirp: "npm:^1.0.3"
morgan: "npm:^1.10.0"
@ -8140,6 +8185,25 @@ __metadata:
languageName: node
linkType: hard
"mathjs@npm:^12.4.2":
version: 12.4.2
resolution: "mathjs@npm:12.4.2"
dependencies:
"@babel/runtime": "npm:^7.24.4"
complex.js: "npm:^2.1.1"
decimal.js: "npm:^10.4.3"
escape-latex: "npm:^1.2.0"
fraction.js: "npm:4.3.4"
javascript-natural-sort: "npm:^0.7.1"
seedrandom: "npm:^3.0.5"
tiny-emitter: "npm:^2.1.0"
typed-function: "npm:^4.1.1"
bin:
mathjs: bin/cli.js
checksum: 4b88ac1b137d00b8f3d66f4d1662d3670399390b59623ecf3ab7d587ba18be7b97ce9c5b07e953029ac75f48567d675c99323889ae231eb071ddd84db5dd699c
languageName: node
linkType: hard
"mdn-data@npm:2.0.14":
version: 2.0.14
resolution: "mdn-data@npm:2.0.14"
@ -10298,6 +10362,13 @@ __metadata:
languageName: node
linkType: hard
"seedrandom@npm:^3.0.5":
version: 3.0.5
resolution: "seedrandom@npm:3.0.5"
checksum: acad5e516c04289f61c2fb9848f449b95f58362b75406b79ec51e101ec885293fc57e3675d2f39f49716336559d7190f7273415d185fead8cd27b171ebf7d8fb
languageName: node
linkType: hard
"semver-compare@npm:^1.0.0":
version: 1.0.0
resolution: "semver-compare@npm:1.0.0"
@ -11209,6 +11280,13 @@ __metadata:
languageName: node
linkType: hard
"tiny-emitter@npm:^2.1.0":
version: 2.1.0
resolution: "tiny-emitter@npm:2.1.0"
checksum: 75633f4de4f47f43af56aff6162f25b87be7efc6f669fda256658f3c3f4a216f23dc0d13200c6fafaaf1b0c7142f0201352fb06aec0b77f68aea96be898f4516
languageName: node
linkType: hard
"tiny-invariant@npm:1.2.0":
version: 1.2.0
resolution: "tiny-invariant@npm:1.2.0"
@ -11536,6 +11614,13 @@ __metadata:
languageName: node
linkType: hard
"typed-function@npm:^4.1.1":
version: 4.1.1
resolution: "typed-function@npm:4.1.1"
checksum: 0ef538d5f02e5c40659cccc14b5f2727f0e4181f11d91bb7897327c33cc2893de7e92343b6b32e1bb15e44a215a1e92e27ab2aa1353b100a9a2697abf2989a0c
languageName: node
linkType: hard
"typedarray-to-buffer@npm:^3.1.5":
version: 3.1.5
resolution: "typedarray-to-buffer@npm:3.1.5"