the transcription goes beside the audio, and nothing else moves

it created a dated directory, moved the recording into it and wrote the
markdown alongside. nobody asked for an archive. the thing you want from a
transcription is the text next to the recording, which is exactly what the
file viewer's transcribe button did — POST with saveNextTo, sibling .md,
done.

overwrites a previous transcription of the same file on purpose: it is
derived, running it again means you wanted it again, and a -2 beside it is
only a second file to choose between.

inline: true, so the file browser runs it and waits rather than making a
background job out of a call that takes seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-24 04:47:05 +00:00
co-authored by Claude Opus 5
parent 3d2e4afd4e
commit 887eed226b
2 changed files with 15 additions and 45 deletions
+9 -39
View File
@@ -7,7 +7,6 @@
// src/servers/api/tasks/task-api-env.ts). The transcribe plugin owns the Whisper contract; this script
// only owns filing the result.
import { rename, mkdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { basename, dirname, join, extname } from 'node:path';
@@ -27,15 +26,6 @@ if (!apiUrl || !apiHost || !authToken) {
const authHeaders = { Authorization: `Bearer ${authToken}`, Host: apiHost };
const slugify = (name: string) =>
name
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
async function getSpokenLanguages(): Promise<string[]> {
try {
const res = await fetch(`${apiUrl}/api/user/settings`, { headers: authHeaders });
@@ -66,17 +56,6 @@ async function transcribe(path: string, spokenLanguages: string[]): Promise<{ te
return { text: json.text ?? '', detectedLanguage: json.detectedLanguage ?? 'unknown' };
}
/** `<archiveDir>/<name>`, walking `-2`, `-3`, … past a collision. Same shape as the file browser's own. */
async function uniqueDir(parentDir: string, name: string): Promise<string> {
let candidate = join(parentDir, name);
let n = 2;
while (existsSync(candidate)) {
candidate = join(parentDir, `${name}-${n}`);
n += 1;
}
return candidate;
}
async function main() {
const path = filePath!;
if (!existsSync(path)) {
@@ -90,26 +69,17 @@ async function main() {
const { text, detectedLanguage } = await transcribe(path, spokenLanguages);
console.log(`Detected language: ${detectedLanguage}`);
const parentDir = dirname(path);
const ext = extname(path);
const base = basename(path, ext);
const now = new Date();
const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
// No model turn to ask for a title (see TASK.md), so the slug comes from the original filename rather
// than the transcription's contents.
const slug = slugify(base) || 'recording';
const archiveDir = await uniqueDir(parentDir, `${date}_${slug}`);
await mkdir(archiveDir, { recursive: true });
const archivedAudioPath = join(archiveDir, basename(path));
await rename(path, archivedAudioPath);
const transcriptPath = join(archiveDir, `${base}.md`);
// Beside the audio, same name, `.md`. Nothing is moved, nothing is created around it — this is the
// behaviour the file viewer's transcribe button had, and the one thing anybody wants from a
// transcription is the text next to the recording it came from.
//
// It overwrites a previous transcription of the same file deliberately: a transcription is derived,
// running it again means you wanted it again, and a `-2` beside it would only be a second file to
// read and choose between.
const transcriptPath = join(dirname(path), `${basename(path, extname(path))}.md`);
await Bun.write(transcriptPath, text);
console.log(`Filed under ${archiveDir}`);
console.log(transcriptPath);
}
main().catch((err) => {