lowercase-hyphenated and equal to its directory, which is what both harnesses
require to LOAD it. carrying a human title there meant the skill was listed to an
agent and then failed to open. the prose moves to `title`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
first dated version. plugins were all sitting on 1.0.0, which said nothing about
when a build was made or which of two copies is newer — the platform already
uses YYYY.MM.DD.NN and this follows it.
the version lives in manifest.ts because a plugin has no package.json; that is
the field the marketplace publishes and the installer compares.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
opencode discovers AGENTS.md and nothing else; claude code discovers CLAUDE.md.
this repo had neither, so an agent sent to change something here arrived with no
guidance at all under either.
it deliberately does NOT repeat README.md. that file says what the plugin is,
for someone deciding whether to install it. this one answers the other question:
i have to change something in here, what will i break.
the facts in it are read from the repository rather than recalled — the panel
keys from web/panels.ts, the tables from db/schema.ts, the permission keys, the
host binaries and the route claims from manifest.ts. presence is the declaration
everywhere in this system, so the file says what the platform will actually
conclude from these files existing.
CLAUDE.md beside it is a pointer, not a copy: two copies drift and the stale one
is the one somebody reads.
tags are how the marketplace is browsed rather than scrolled: a list of them on
the left, the plugins that carry one on the right. a closed vocabulary
(PLUGIN_TAGS in the platform) because free text becomes homelab, home-lab, Home
Lab and selfhosted inside a month, and a filter list of synonyms is worse than
no filter at all.
declared here rather than curated in the catalogue, so a plugin can say what it
is and nobody edits a table when one ships. the marketplace caches it on sync,
exactly as it already does for label and summary.
every transcription on this machine failed with "Language detection failed
(400)" before it started. the server was fine — it had answered /inference
correctly minutes earlier — and the platform was fine. the plugin's FIRST
call sends detect_language=true, which is a whisper.cpp parameter, and
whisperx answers 400 to it.
so a pass that exists to HELP became the only thing that could fail, on a
backend that does its own detection anyway. whisperx logs "No language
specified, language will be detected for each audio file" and gets it
right per file.
a backend refusing that request is telling us it detects for itself. skip
the pass, send no `language`, and read back whatever the transcription
reports. the two-pass path is untouched for whisper.cpp, which needs it,
and the spoken-language tie-break still applies wherever detection runs.
no prompt priming when the language is unknown either — TRANSCRIPT_PROMPTS
is keyed by language, and priming with the wrong one is worse than not
priming.
output_dir, optional. absent, the text lands beside the recording as it
always has — that is what a person right-clicking a memo wants and it is
not changing. given, the transcript goes there instead, and the directory
is created if it does not exist.
for callers that are not a person clicking a file. a commander keeps its
results in its own directory, and the alternative was making every
commander move the file afterwards: a step in the loop forever, plus a
window where the file sits in the folder being watched.
marked `hidden` so the file browser neither shows it nor counts it as
something to ask about — without that, declaring it turned a direct
right-click run back into a modal with a field nobody clicking a file has
an answer for.
A probe on the torch stage worker (~/dockers/torch-worker) runs beside every
transcription and decides what the recording is: solo dictation returns as
paragraphs exactly as before, several voices return speaker-labeled with
conversation: true. Nobody declares which — the caller cannot know.
Transcription and the worker each get a serial FIFO queue on their own
resource, so the probe costs a solo memo nothing and multi-user traffic
stays fair. The worker is optional and best-effort throughout: unreachable
or failing, everything still transcribes, just never labeled.
Speaker attribution is segment-granularity by maximal turn overlap —
whisper.cpp's own timestamps, no alignment stage. If boundaries prove
sloppy, wav2vec2 alignment returns as a worker endpoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
this holds a connection open while whisper works, and nothing flows on it
meanwhile — exactly what an idle timeout kills. unset, Bun drops the
platform's connection at around 255s.
it was the last of three hops still doing it. the proxy's outbound fetch
and the platform's inbound timeout were both fixed earlier today and a
42-minute recording still died at 4m16s. whisperx went on and finished it;
the result was discarded.
with diarization on, align runs at roughly 1.5x the audio duration, so an
hour-long episode needs ninety minutes. no ceiling is both safe and high
enough, and 255 is Bun's maximum anyway — "raise it" was never available.
0 disables it.
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 plugin ships no web/, so there was nowhere to set its server URL — the only
way in was an HTTP call somebody had to already know about. That is not a
setting, it is a secret handshake, and it is the state this shipped in.
Declared rather than rendered: the platform draws the form on the plugin detail
page and PUTs to this plugin's own /config. It stores nothing, so the URL stays
where this plugin already kept it and the validation against the server still
happens here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
Moves platform STT out into a plugin that owns the whole whisper.cpp contract: sidecar,
config (a JSON file under DATA_PATH/transcribe/, validated against the server before
storing), a thin proxy router, a skill so an agent knows the contract without being told,
and a script-mode file-browser task (not agentic — no model turn per audio file).
sidecar/transcribe.ts and its test moved unchanged from src/servers/api/stt/ — every
constant in it has a reason attached and none of it was touched.
The platform-side removal (routes, dead useAudioRecording path, Settings screen, locale
keys) lands in a separate, uncommitted change in the platform repo for review.
Not yet reachable from the file browser: the platform does not load tasks from a plugin's
directory yet (skills/affordances/notifications do). Ships anyway per instructions.
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043