feat(backend,frontend,mcp): any voice or round video can be transcribed on request, media is found by its bytes

This commit is contained in:
hh committed 2026-10-28 01:35:56 +00:00
1 parent c2180e80ad
commit 2dd4e589c3
17 files changed
+523 -159

No files matched your search

@@ -0,0 +1,65 @@
import { requestTranscription } from "#lib/api/media.js";
import { t } from "#lib/i18n/locale.svelte.js";
import type { MessageKey } from "#lib/i18n/messages.js";
import { toasts } from "#lib/stores/toasts.svelte.js";
const REFUSALS: Record<string, MessageKey> = {
empty: "media.transcribeEmpty",
no_premium: "media.transcribeNoPremium",
pending: "media.transcribePending",
too_long: "media.transcribeTooLong",
};
export class Transcript {
readonly #chatId: () => number;
readonly #messageId: () => number;
readonly #initial: () => string | null;
fetched = $state<string | null>(null);
pending = $state(false);
shown = $state(false);
constructor(
chatId: () => number,
messageId: () => number,
initial: () => string | null
) {
this.#chatId = chatId;
this.#messageId = messageId;
this.#initial = initial;
}
get text(): string | null {
return this.fetched ?? this.#initial();
}
get open(): boolean {
return this.shown && Boolean(this.text);
}
async toggle(): Promise<void> {
if (this.text) {
this.shown = !this.shown;
return;
}
if (this.pending) {
return;
}
this.pending = true;
try {
const outcome = await requestTranscription(
this.#chatId(),
this.#messageId()
);
if ("text" in outcome) {
this.fetched = outcome.text;
this.shown = true;
} else {
toasts.error(t(REFUSALS[outcome.error] ?? "media.transcribeFailed"));
}
} catch {
toasts.error(t("media.transcribeFailed"));
} finally {
this.pending = false;
}
}
}