drop the built-in Extract Audio entry, and its cache
Extract Audio now exists as a task, with recursion, multi-select scoping, a format choice and multi-track handling — none of which the one-shot menu entry had, since it always produced a single mp3. /extract-audio stays because the file viewer's button plays its output rather than saving it beside the video, but it no longer returns a cached file: like transcription and OCR, it always redoes the work. The path is now named outRel, since it is an output location rather than a cache. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9d01000578
commit
0c7c015fc3
@@ -849,14 +849,14 @@ router.post('/extract-audio', async (ctx) => {
|
|||||||
const s = await stat(absPath);
|
const s = await stat(absPath);
|
||||||
if (s.isDirectory()) throw errors.BAD_REQUEST('Cannot extract audio from a directory');
|
if (s.isDirectory()) throw errors.BAD_REQUEST('Cannot extract audio from a directory');
|
||||||
|
|
||||||
|
// No caching: the extraction is always redone and overwrites the previous result. The output still
|
||||||
|
// lives under the user's data dir because this endpoint backs the file viewer's Extract Audio
|
||||||
|
// button, which plays it rather than saving it beside the video — the Extract Audio task is what
|
||||||
|
// writes into the user's folders.
|
||||||
const userDataDir = getUserDataDir(user.email);
|
const userDataDir = getUserDataDir(user.email);
|
||||||
const { dir, name } = parsePath(filePath.replace(/^\/+/, ''));
|
const { dir, name } = parsePath(filePath.replace(/^\/+/, ''));
|
||||||
const cacheRel = dir ? `cache/audio/${dir}/${name}.mp3` : `cache/audio/${name}.mp3`;
|
const outRel = dir ? `cache/audio/${dir}/${name}.mp3` : `cache/audio/${name}.mp3`;
|
||||||
const cacheAbs = resolve(userDataDir, cacheRel);
|
const cacheAbs = resolve(userDataDir, outRel);
|
||||||
|
|
||||||
if (existsSync(cacheAbs)) {
|
|
||||||
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data', cached: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
await mkdir(dirname(cacheAbs), { recursive: true });
|
await mkdir(dirname(cacheAbs), { recursive: true });
|
||||||
|
|
||||||
@@ -871,7 +871,7 @@ router.post('/extract-audio', async (ctx) => {
|
|||||||
throw errors.BAD_REQUEST(stderr.trim() || 'Audio extraction failed');
|
throw errors.BAD_REQUEST(stderr.trim() || 'Audio extraction failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data', cached: false });
|
return ctx.json({ audioPath: outRel, audioRoot: 'user-data', cached: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract archive (zip, tar, 7z, rar) into a sibling folder
|
// Extract archive (zip, tar, 7z, rar) into a sibling folder
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
|||||||
setRenamingName,
|
setRenamingName,
|
||||||
handleReadAloud,
|
handleReadAloud,
|
||||||
handleOcr,
|
handleOcr,
|
||||||
handleExtractAudio,
|
|
||||||
handleExtract,
|
handleExtract,
|
||||||
handlePlay,
|
handlePlay,
|
||||||
getMatchingTaskGroups,
|
getMatchingTaskGroups,
|
||||||
@@ -180,7 +179,6 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
|
|||||||
onRenamingChange={setRenamingName}
|
onRenamingChange={setRenamingName}
|
||||||
onReadAloud={handleReadAloud}
|
onReadAloud={handleReadAloud}
|
||||||
onOcr={handleOcr}
|
onOcr={handleOcr}
|
||||||
onExtractAudio={handleExtractAudio}
|
|
||||||
onExtract={handleExtract}
|
onExtract={handleExtract}
|
||||||
onPlay={handlePlay}
|
onPlay={handlePlay}
|
||||||
taskGroups={getMatchingTaskGroups(entry.name, entry.type)}
|
taskGroups={getMatchingTaskGroups(entry.name, entry.type)}
|
||||||
|
|||||||
+2
-23
@@ -13,7 +13,6 @@ import {
|
|||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Volume2,
|
Volume2,
|
||||||
ScanText,
|
ScanText,
|
||||||
AudioLines,
|
|
||||||
FolderArchive,
|
FolderArchive,
|
||||||
ClipboardCopy,
|
ClipboardCopy,
|
||||||
Music,
|
Music,
|
||||||
@@ -64,7 +63,6 @@ export type FileItemProps = {
|
|||||||
onRenamingChange: (name: string | null) => void;
|
onRenamingChange: (name: string | null) => void;
|
||||||
onReadAloud: (entry: DirEntry) => void;
|
onReadAloud: (entry: DirEntry) => void;
|
||||||
onOcr: (entry: DirEntry) => void;
|
onOcr: (entry: DirEntry) => void;
|
||||||
onExtractAudio: (entry: DirEntry) => void;
|
|
||||||
onExtract: (entry: DirEntry) => void;
|
onExtract: (entry: DirEntry) => void;
|
||||||
onPlay: (entry: DirEntry) => void;
|
onPlay: (entry: DirEntry) => void;
|
||||||
taskGroups: TaskGroup[];
|
taskGroups: TaskGroup[];
|
||||||
@@ -96,7 +94,6 @@ type MenuItemsProps = {
|
|||||||
onDownload: (e: DirEntry) => void;
|
onDownload: (e: DirEntry) => void;
|
||||||
onReadAloud: (e: DirEntry) => void;
|
onReadAloud: (e: DirEntry) => void;
|
||||||
onOcr: (e: DirEntry) => void;
|
onOcr: (e: DirEntry) => void;
|
||||||
onExtractAudio: (e: DirEntry) => void;
|
|
||||||
onExtract: (e: DirEntry) => void;
|
onExtract: (e: DirEntry) => void;
|
||||||
onPlay: (e: DirEntry) => void;
|
onPlay: (e: DirEntry) => void;
|
||||||
onCut: () => void;
|
onCut: () => void;
|
||||||
@@ -116,7 +113,6 @@ const DropdownMenuItems = ({
|
|||||||
onDownload,
|
onDownload,
|
||||||
onReadAloud,
|
onReadAloud,
|
||||||
onOcr,
|
onOcr,
|
||||||
onExtractAudio,
|
|
||||||
onExtract,
|
onExtract,
|
||||||
onPlay,
|
onPlay,
|
||||||
onCut,
|
onCut,
|
||||||
@@ -128,12 +124,11 @@ const DropdownMenuItems = ({
|
|||||||
const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
|
const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
|
||||||
const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
|
const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
|
||||||
const showOcr = fileType === 'image';
|
const showOcr = fileType === 'image';
|
||||||
const showExtractAudio = fileType === 'video';
|
|
||||||
const showExtract = fileType === 'archive';
|
const showExtract = fileType === 'archive';
|
||||||
const showPlay = fileType === 'audio' || entry.type === 'directory';
|
const showPlay = fileType === 'audio' || entry.type === 'directory';
|
||||||
|
|
||||||
const hasTasks = taskGroups.length > 0;
|
const hasTasks = taskGroups.length > 0;
|
||||||
const hasActions = showPlay || showReadAloud || showOcr || showExtractAudio || showExtract || hasTasks;
|
const hasActions = showPlay || showReadAloud || showOcr || showExtract || hasTasks;
|
||||||
// Only nest when more than one category matched — a file usually matches a single category, and
|
// Only nest when more than one category matched — a file usually matches a single category, and
|
||||||
// Run Task > Video > Convert would just add a hop.
|
// Run Task > Video > Convert would just add a hop.
|
||||||
const nestTasks = taskGroups.length > 1;
|
const nestTasks = taskGroups.length > 1;
|
||||||
@@ -159,12 +154,6 @@ const DropdownMenuItems = ({
|
|||||||
Extract Text (OCR)
|
Extract Text (OCR)
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{showExtractAudio && (
|
|
||||||
<DropdownMenuItem onClick={() => onExtractAudio(entry)} className="cursor-pointer">
|
|
||||||
<AudioLines className="mr-2 h-4 w-4" />
|
|
||||||
Extract Audio
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{showExtract && (
|
{showExtract && (
|
||||||
<DropdownMenuItem onClick={() => onExtract(entry)} className="cursor-pointer">
|
<DropdownMenuItem onClick={() => onExtract(entry)} className="cursor-pointer">
|
||||||
<FolderArchive className="mr-2 h-4 w-4" />
|
<FolderArchive className="mr-2 h-4 w-4" />
|
||||||
@@ -260,7 +249,6 @@ const ContextMenuItems = ({
|
|||||||
onDownload,
|
onDownload,
|
||||||
onReadAloud,
|
onReadAloud,
|
||||||
onOcr,
|
onOcr,
|
||||||
onExtractAudio,
|
|
||||||
onExtract,
|
onExtract,
|
||||||
onPlay,
|
onPlay,
|
||||||
onCut,
|
onCut,
|
||||||
@@ -272,12 +260,11 @@ const ContextMenuItems = ({
|
|||||||
const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
|
const fileType = entry.type === 'file' ? getFileType(entry.name) : null;
|
||||||
const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
|
const showReadAloud = fileType === 'markdown' || fileType === 'code' || fileType === 'text';
|
||||||
const showOcr = fileType === 'image';
|
const showOcr = fileType === 'image';
|
||||||
const showExtractAudio = fileType === 'video';
|
|
||||||
const showExtract = fileType === 'archive';
|
const showExtract = fileType === 'archive';
|
||||||
const showPlay = fileType === 'audio' || entry.type === 'directory';
|
const showPlay = fileType === 'audio' || entry.type === 'directory';
|
||||||
|
|
||||||
const hasTasks = taskGroups.length > 0;
|
const hasTasks = taskGroups.length > 0;
|
||||||
const hasActions = showPlay || showReadAloud || showOcr || showExtractAudio || showExtract || hasTasks;
|
const hasActions = showPlay || showReadAloud || showOcr || showExtract || hasTasks;
|
||||||
// Only nest when more than one category matched — a file usually matches a single category, and
|
// Only nest when more than one category matched — a file usually matches a single category, and
|
||||||
// Run Task > Video > Convert would just add a hop.
|
// Run Task > Video > Convert would just add a hop.
|
||||||
const nestTasks = taskGroups.length > 1;
|
const nestTasks = taskGroups.length > 1;
|
||||||
@@ -303,12 +290,6 @@ const ContextMenuItems = ({
|
|||||||
Extract Text (OCR)
|
Extract Text (OCR)
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)}
|
)}
|
||||||
{showExtractAudio && (
|
|
||||||
<ContextMenuItem onClick={() => onExtractAudio(entry)} className="cursor-pointer">
|
|
||||||
<AudioLines className="mr-2 h-4 w-4" />
|
|
||||||
Extract Audio
|
|
||||||
</ContextMenuItem>
|
|
||||||
)}
|
|
||||||
{showExtract && (
|
{showExtract && (
|
||||||
<ContextMenuItem onClick={() => onExtract(entry)} className="cursor-pointer">
|
<ContextMenuItem onClick={() => onExtract(entry)} className="cursor-pointer">
|
||||||
<FolderArchive className="mr-2 h-4 w-4" />
|
<FolderArchive className="mr-2 h-4 w-4" />
|
||||||
@@ -513,7 +494,6 @@ export const FileItem = ({
|
|||||||
onRenamingChange,
|
onRenamingChange,
|
||||||
onReadAloud,
|
onReadAloud,
|
||||||
onOcr,
|
onOcr,
|
||||||
onExtractAudio,
|
|
||||||
onExtract,
|
onExtract,
|
||||||
onPlay,
|
onPlay,
|
||||||
taskGroups,
|
taskGroups,
|
||||||
@@ -595,7 +575,6 @@ export const FileItem = ({
|
|||||||
onDownload,
|
onDownload,
|
||||||
onReadAloud,
|
onReadAloud,
|
||||||
onOcr,
|
onOcr,
|
||||||
onExtractAudio,
|
|
||||||
onExtract,
|
onExtract,
|
||||||
onPlay,
|
onPlay,
|
||||||
onCut,
|
onCut,
|
||||||
|
|||||||
@@ -457,18 +457,6 @@ export const useFileBrowserApp = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExtractAudio = async (entry: DirEntry) => {
|
|
||||||
const filePath = entryPath(entry.name);
|
|
||||||
const toastId = toast.loading('Extracting audio from video...');
|
|
||||||
try {
|
|
||||||
const { audioPath, audioRoot } = await files.extractAudio(filePath);
|
|
||||||
toast.dismiss(toastId);
|
|
||||||
setSearchParams({ view: filePath, ephemeral: audioPath, ephemeralRoot: audioRoot });
|
|
||||||
} catch {
|
|
||||||
toast.error('Failed to extract audio from video', { id: toastId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlay = (entry: DirEntry) => {
|
const handlePlay = (entry: DirEntry) => {
|
||||||
const filePath = entryPath(entry.name);
|
const filePath = entryPath(entry.name);
|
||||||
setSearchParams({ play: filePath });
|
setSearchParams({ play: filePath });
|
||||||
@@ -765,7 +753,6 @@ export const useFileBrowserApp = (
|
|||||||
handleCreateDashboardHere,
|
handleCreateDashboardHere,
|
||||||
handleReadAloud,
|
handleReadAloud,
|
||||||
handleOcr,
|
handleOcr,
|
||||||
handleExtractAudio,
|
|
||||||
handleExtract,
|
handleExtract,
|
||||||
handlePlay,
|
handlePlay,
|
||||||
handleGitClone,
|
handleGitClone,
|
||||||
|
|||||||
Reference in New Issue
Block a user