save ocr/tts/transcription results next to source file from context menu
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -391,7 +391,7 @@ router.post('/save-result', async (ctx) => {
|
|||||||
// Text-to-speech with caching
|
// Text-to-speech with caching
|
||||||
router.post('/tts', async (ctx) => {
|
router.post('/tts', async (ctx) => {
|
||||||
const user = ctx.get('user');
|
const user = ctx.get('user');
|
||||||
const { path: filePath, root } = ctx.get('body') as { path: string; root?: string };
|
const { path: filePath, root, saveNextTo } = ctx.get('body') as { path: string; root?: string; saveNextTo?: boolean };
|
||||||
if (!filePath) throw errors.BAD_REQUEST('path is required');
|
if (!filePath) throw errors.BAD_REQUEST('path is required');
|
||||||
|
|
||||||
const rootDir = getRootDir(user, root);
|
const rootDir = getRootDir(user, root);
|
||||||
@@ -411,7 +411,22 @@ router.post('/tts', async (ctx) => {
|
|||||||
const cacheRel = dir ? `cache/tts/${voicePrefix}${dir}/${name}.mp3` : `cache/tts/${voicePrefix}${name}.mp3`;
|
const cacheRel = dir ? `cache/tts/${voicePrefix}${dir}/${name}.mp3` : `cache/tts/${voicePrefix}${name}.mp3`;
|
||||||
const cacheAbs = resolve(userDataDir, cacheRel);
|
const cacheAbs = resolve(userDataDir, cacheRel);
|
||||||
|
|
||||||
|
if (saveNextTo) {
|
||||||
|
const siblingRel = dir ? `${dir}/${name}.mp3` : `${name}.mp3`;
|
||||||
|
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||||
|
if (existsSync(siblingAbs)) {
|
||||||
|
return ctx.json({ audioPath: `/${siblingRel}`, audioRoot: root ?? 'home', cached: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (existsSync(cacheAbs)) {
|
if (existsSync(cacheAbs)) {
|
||||||
|
if (saveNextTo) {
|
||||||
|
const siblingRel = dir ? `${dir}/${name}.mp3` : `${name}.mp3`;
|
||||||
|
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||||
|
const cached = await readFile(cacheAbs);
|
||||||
|
await Bun.write(siblingAbs, cached);
|
||||||
|
return ctx.json({ audioPath: `/${siblingRel}`, audioRoot: root ?? 'home', cached: true });
|
||||||
|
}
|
||||||
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data' });
|
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +456,13 @@ router.post('/tts', async (ctx) => {
|
|||||||
const buffer = await res.arrayBuffer();
|
const buffer = await res.arrayBuffer();
|
||||||
await Bun.write(cacheAbs, buffer);
|
await Bun.write(cacheAbs, buffer);
|
||||||
|
|
||||||
|
if (saveNextTo) {
|
||||||
|
const siblingRel = dir ? `${dir}/${name}.mp3` : `${name}.mp3`;
|
||||||
|
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||||
|
await Bun.write(siblingAbs, buffer);
|
||||||
|
return ctx.json({ audioPath: `/${siblingRel}`, audioRoot: root ?? 'home', cached: false });
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data' });
|
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -495,7 +517,7 @@ router.post('/tts-text', async (ctx) => {
|
|||||||
// OCR image via vision model with caching
|
// OCR image via vision model with caching
|
||||||
router.post('/ocr', async (ctx) => {
|
router.post('/ocr', async (ctx) => {
|
||||||
const user = ctx.get('user');
|
const user = ctx.get('user');
|
||||||
const { path: filePath, root } = ctx.get('body') as { path: string; root?: string };
|
const { path: filePath, root, saveNextTo } = ctx.get('body') as { path: string; root?: string; saveNextTo?: boolean };
|
||||||
if (!filePath) throw errors.BAD_REQUEST('path is required');
|
if (!filePath) throw errors.BAD_REQUEST('path is required');
|
||||||
|
|
||||||
const rootDir = getRootDir(user, root);
|
const rootDir = getRootDir(user, root);
|
||||||
@@ -508,7 +530,23 @@ router.post('/ocr', async (ctx) => {
|
|||||||
const cacheRel = dir ? `cache/ocr/${dir}/${name}.md` : `cache/ocr/${name}.md`;
|
const cacheRel = dir ? `cache/ocr/${dir}/${name}.md` : `cache/ocr/${name}.md`;
|
||||||
const cacheAbs = resolve(userDataDir, cacheRel);
|
const cacheAbs = resolve(userDataDir, cacheRel);
|
||||||
|
|
||||||
|
// When saving next to file, check if the sibling .md already exists
|
||||||
|
if (saveNextTo) {
|
||||||
|
const siblingRel = dir ? `${dir}/${name}.md` : `${name}.md`;
|
||||||
|
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||||
|
if (existsSync(siblingAbs)) {
|
||||||
|
return ctx.json({ ocrPath: `/${siblingRel}`, ocrRoot: root ?? 'home', cached: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (existsSync(cacheAbs)) {
|
if (existsSync(cacheAbs)) {
|
||||||
|
if (saveNextTo) {
|
||||||
|
const text = await readFile(cacheAbs, 'utf-8');
|
||||||
|
const siblingRel = dir ? `${dir}/${name}.md` : `${name}.md`;
|
||||||
|
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||||
|
await Bun.write(siblingAbs, text);
|
||||||
|
return ctx.json({ ocrPath: `/${siblingRel}`, ocrRoot: root ?? 'home', cached: true });
|
||||||
|
}
|
||||||
const text = await readFile(cacheAbs, 'utf-8');
|
const text = await readFile(cacheAbs, 'utf-8');
|
||||||
return ctx.json({ text, ocrPath: cacheRel, ocrRoot: 'user-data', cached: true });
|
return ctx.json({ text, ocrPath: cacheRel, ocrRoot: 'user-data', cached: true });
|
||||||
}
|
}
|
||||||
@@ -566,6 +604,13 @@ router.post('/ocr', async (ctx) => {
|
|||||||
await mkdir(dirname(cacheAbs), { recursive: true });
|
await mkdir(dirname(cacheAbs), { recursive: true });
|
||||||
await Bun.write(cacheAbs, text);
|
await Bun.write(cacheAbs, text);
|
||||||
|
|
||||||
|
if (saveNextTo) {
|
||||||
|
const siblingRel = dir ? `${dir}/${name}.md` : `${name}.md`;
|
||||||
|
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||||
|
await Bun.write(siblingAbs, text);
|
||||||
|
return ctx.json({ ocrPath: `/${siblingRel}`, ocrRoot: root ?? 'home', cached: false });
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json({ text, ocrPath: cacheRel, ocrRoot: 'user-data', cached: false });
|
return ctx.json({ text, ocrPath: cacheRel, ocrRoot: 'user-data', cached: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -696,7 +741,7 @@ router.post('/extract', async (ctx) => {
|
|||||||
// Workflow: detect language → check user's spoken languages → translate if needed → transcribe
|
// Workflow: detect language → check user's spoken languages → translate if needed → transcribe
|
||||||
router.post('/transcribe', async (ctx) => {
|
router.post('/transcribe', async (ctx) => {
|
||||||
const user = ctx.get('user');
|
const user = ctx.get('user');
|
||||||
const { path: filePath, root } = ctx.get('body') as { path: string; root?: string };
|
const { path: filePath, root, saveNextTo } = ctx.get('body') as { path: string; root?: string; saveNextTo?: boolean };
|
||||||
if (!filePath) throw errors.BAD_REQUEST('path is required');
|
if (!filePath) throw errors.BAD_REQUEST('path is required');
|
||||||
|
|
||||||
const rootDir = getRootDir(user, root);
|
const rootDir = getRootDir(user, root);
|
||||||
@@ -709,7 +754,22 @@ router.post('/transcribe', async (ctx) => {
|
|||||||
const cacheRel = dir ? `cache/transcriptions/${dir}/${name}.md` : `cache/transcriptions/${name}.md`;
|
const cacheRel = dir ? `cache/transcriptions/${dir}/${name}.md` : `cache/transcriptions/${name}.md`;
|
||||||
const cacheAbs = resolve(userDataDir, cacheRel);
|
const cacheAbs = resolve(userDataDir, cacheRel);
|
||||||
|
|
||||||
|
if (saveNextTo) {
|
||||||
|
const siblingRel = dir ? `${dir}/${name}.md` : `${name}.md`;
|
||||||
|
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||||
|
if (existsSync(siblingAbs)) {
|
||||||
|
return ctx.json({ transcriptionPath: `/${siblingRel}`, transcriptionRoot: root ?? 'home', cached: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (existsSync(cacheAbs)) {
|
if (existsSync(cacheAbs)) {
|
||||||
|
if (saveNextTo) {
|
||||||
|
const text = await readFile(cacheAbs, 'utf-8');
|
||||||
|
const siblingRel = dir ? `${dir}/${name}.md` : `${name}.md`;
|
||||||
|
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||||
|
await Bun.write(siblingAbs, text);
|
||||||
|
return ctx.json({ transcriptionPath: `/${siblingRel}`, transcriptionRoot: root ?? 'home', cached: true });
|
||||||
|
}
|
||||||
return ctx.json({ transcriptionPath: cacheRel, transcriptionRoot: 'user-data', cached: true });
|
return ctx.json({ transcriptionPath: cacheRel, transcriptionRoot: 'user-data', cached: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,6 +822,13 @@ router.post('/transcribe', async (ctx) => {
|
|||||||
await mkdir(dirname(cacheAbs), { recursive: true });
|
await mkdir(dirname(cacheAbs), { recursive: true });
|
||||||
await Bun.write(cacheAbs, text);
|
await Bun.write(cacheAbs, text);
|
||||||
|
|
||||||
|
if (saveNextTo) {
|
||||||
|
const siblingRel = dir ? `${dir}/${name}.md` : `${name}.md`;
|
||||||
|
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||||
|
await Bun.write(siblingAbs, text);
|
||||||
|
return ctx.json({ transcriptionPath: `/${siblingRel}`, transcriptionRoot: root ?? 'home', cached: false });
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json({ transcriptionPath: cacheRel, transcriptionRoot: 'user-data', cached: false });
|
return ctx.json({ transcriptionPath: cacheRel, transcriptionRoot: 'user-data', cached: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -413,9 +413,10 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, initi
|
|||||||
const filePath = entryPath(entry.name);
|
const filePath = entryPath(entry.name);
|
||||||
const toastId = toast.loading('Generating speech audio...');
|
const toastId = toast.loading('Generating speech audio...');
|
||||||
try {
|
try {
|
||||||
const { audioPath, audioRoot } = await files.tts(filePath);
|
const { audioPath } = await files.tts(filePath, { saveNextTo: true });
|
||||||
toast.dismiss(toastId);
|
toast.dismiss(toastId);
|
||||||
setSearchParams({ view: filePath, ephemeral: audioPath, ephemeralRoot: audioRoot });
|
refresh();
|
||||||
|
setSearchParams({ view: audioPath });
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to generate speech audio', { id: toastId });
|
toast.error('Failed to generate speech audio', { id: toastId });
|
||||||
}
|
}
|
||||||
@@ -425,9 +426,10 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, initi
|
|||||||
const filePath = entryPath(entry.name);
|
const filePath = entryPath(entry.name);
|
||||||
const toastId = toast.loading('Extracting text from image...');
|
const toastId = toast.loading('Extracting text from image...');
|
||||||
try {
|
try {
|
||||||
const { ocrPath, ocrRoot } = await files.ocr(filePath);
|
const { ocrPath } = await files.ocr(filePath, { saveNextTo: true });
|
||||||
toast.dismiss(toastId);
|
toast.dismiss(toastId);
|
||||||
setSearchParams({ view: filePath, ephemeral: ocrPath, ephemeralRoot: ocrRoot });
|
refresh();
|
||||||
|
setSearchParams({ view: ocrPath });
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to extract text from image', { id: toastId });
|
toast.error('Failed to extract text from image', { id: toastId });
|
||||||
}
|
}
|
||||||
@@ -437,9 +439,10 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, initi
|
|||||||
const filePath = entryPath(entry.name);
|
const filePath = entryPath(entry.name);
|
||||||
const toastId = toast.loading('Transcribing audio...');
|
const toastId = toast.loading('Transcribing audio...');
|
||||||
try {
|
try {
|
||||||
const { transcriptionPath, transcriptionRoot } = await files.transcribe(filePath);
|
const { transcriptionPath } = await files.transcribe(filePath, { saveNextTo: true });
|
||||||
toast.dismiss(toastId);
|
toast.dismiss(toastId);
|
||||||
setSearchParams({ view: filePath, ephemeral: transcriptionPath, ephemeralRoot: transcriptionRoot });
|
refresh();
|
||||||
|
setSearchParams({ view: transcriptionPath });
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to transcribe audio', { id: toastId });
|
toast.error('Failed to transcribe audio', { id: toastId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,17 +48,17 @@ export const useFilesAPI = (root: string = 'home') => {
|
|||||||
downloadVideo: (url: string, path: string, audioOnly: boolean) =>
|
downloadVideo: (url: string, path: string, audioOnly: boolean) =>
|
||||||
client.post(withRoot('/file-browser/download-video'), { url, path, audioOnly }),
|
client.post(withRoot('/file-browser/download-video'), { url, path, audioOnly }),
|
||||||
|
|
||||||
tts: (path: string) =>
|
tts: (path: string, opts?: { saveNextTo?: boolean }) =>
|
||||||
client.post<{ audioPath: string; audioRoot: string }>('/file-browser/tts', { path, root }),
|
client.post<{ audioPath: string; audioRoot: string }>('/file-browser/tts', { path, root, ...opts }),
|
||||||
|
|
||||||
ttsText: (id: string, text: string) =>
|
ttsText: (id: string, text: string) =>
|
||||||
client.post<{ audioPath: string; audioRoot: string }>('/file-browser/tts-text', { id, text }),
|
client.post<{ audioPath: string; audioRoot: string }>('/file-browser/tts-text', { id, text }),
|
||||||
|
|
||||||
ocr: (path: string) =>
|
ocr: (path: string, opts?: { saveNextTo?: boolean }) =>
|
||||||
client.post<{ ocrPath: string; ocrRoot: string }>('/file-browser/ocr', { path, root }),
|
client.post<{ ocrPath: string; ocrRoot: string }>('/file-browser/ocr', { path, root, ...opts }),
|
||||||
|
|
||||||
transcribe: (path: string) =>
|
transcribe: (path: string, opts?: { saveNextTo?: boolean }) =>
|
||||||
client.post<{ transcriptionPath: string; transcriptionRoot: string }>('/file-browser/transcribe', { path, root }),
|
client.post<{ transcriptionPath: string; transcriptionRoot: string }>('/file-browser/transcribe', { path, root, ...opts }),
|
||||||
|
|
||||||
extractAudio: (path: string) =>
|
extractAudio: (path: string) =>
|
||||||
client.post<{ audioPath: string; audioRoot: string }>('/file-browser/extract-audio', { path, root }),
|
client.post<{ audioPath: string; audioRoot: string }>('/file-browser/extract-audio', { path, root }),
|
||||||
|
|||||||
Reference in New Issue
Block a user