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:
@@ -4,7 +4,7 @@ description: Transcribe an audio file to text over your own whisper.cpp server,
|
|||||||
category: Media
|
category: Media
|
||||||
version: 1
|
version: 1
|
||||||
mode: script
|
mode: script
|
||||||
inline: false
|
inline: true
|
||||||
language: typescript
|
language: typescript
|
||||||
tags:
|
tags:
|
||||||
- audio
|
- audio
|
||||||
@@ -38,11 +38,11 @@ model call spent re-deriving what the plugin already returns.
|
|||||||
(`GET /api/user/settings`).
|
(`GET /api/user/settings`).
|
||||||
2. `POST` the file to `/api/transcribe/transcribe`. The plugin detects the language and transcribes
|
2. `POST` the file to `/api/transcribe/transcribe`. The plugin detects the language and transcribes
|
||||||
faithfully in it — see `plugins/transcribe/skills/transcribe/SKILL.md` for the contract.
|
faithfully in it — see `plugins/transcribe/skills/transcribe/SKILL.md` for the contract.
|
||||||
3. Create a sibling directory named `<date>_<slug>`, where `<date>` is today in `YYYYMMDD` and `<slug>`
|
3. Write the transcription beside the audio file, same name, `.md`. Nothing is moved and nothing is
|
||||||
is derived from the original file's own name (not a generated title — a script task gets no model
|
created around it.
|
||||||
turn to ask for one).
|
|
||||||
4. Move the original audio file into that directory and save the transcription beside it as a
|
It overwrites a previous transcription of the same file, deliberately: a transcription is derived, and
|
||||||
Markdown file with the same base name.
|
running it again means you wanted it again.
|
||||||
|
|
||||||
## Before it runs
|
## Before it runs
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
// src/servers/api/tasks/task-api-env.ts). The transcribe plugin owns the Whisper contract; this script
|
// src/servers/api/tasks/task-api-env.ts). The transcribe plugin owns the Whisper contract; this script
|
||||||
// only owns filing the result.
|
// only owns filing the result.
|
||||||
|
|
||||||
import { rename, mkdir } from 'node:fs/promises';
|
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { basename, dirname, join, extname } from 'node:path';
|
import { basename, dirname, join, extname } from 'node:path';
|
||||||
|
|
||||||
@@ -27,15 +26,6 @@ if (!apiUrl || !apiHost || !authToken) {
|
|||||||
|
|
||||||
const authHeaders = { Authorization: `Bearer ${authToken}`, Host: apiHost };
|
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[]> {
|
async function getSpokenLanguages(): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${apiUrl}/api/user/settings`, { headers: authHeaders });
|
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' };
|
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() {
|
async function main() {
|
||||||
const path = filePath!;
|
const path = filePath!;
|
||||||
if (!existsSync(path)) {
|
if (!existsSync(path)) {
|
||||||
@@ -90,26 +69,17 @@ async function main() {
|
|||||||
const { text, detectedLanguage } = await transcribe(path, spokenLanguages);
|
const { text, detectedLanguage } = await transcribe(path, spokenLanguages);
|
||||||
console.log(`Detected language: ${detectedLanguage}`);
|
console.log(`Detected language: ${detectedLanguage}`);
|
||||||
|
|
||||||
const parentDir = dirname(path);
|
// Beside the audio, same name, `.md`. Nothing is moved, nothing is created around it — this is the
|
||||||
const ext = extname(path);
|
// behaviour the file viewer's transcribe button had, and the one thing anybody wants from a
|
||||||
const base = basename(path, ext);
|
// transcription is the text next to the recording it came from.
|
||||||
|
//
|
||||||
const now = new Date();
|
// It overwrites a previous transcription of the same file deliberately: a transcription is derived,
|
||||||
const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
// running it again means you wanted it again, and a `-2` beside it would only be a second file to
|
||||||
// No model turn to ask for a title (see TASK.md), so the slug comes from the original filename rather
|
// read and choose between.
|
||||||
// than the transcription's contents.
|
const transcriptPath = join(dirname(path), `${basename(path, extname(path))}.md`);
|
||||||
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`);
|
|
||||||
await Bun.write(transcriptPath, text);
|
await Bun.write(transcriptPath, text);
|
||||||
|
|
||||||
console.log(`Filed under ${archiveDir}`);
|
console.log(transcriptPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user