feat(workspace): voice notes via /audio (M3)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m10s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m10s
- /audio (only in an empty bullet) shows a recording bubble: green round button starts, it turns red with a live wave and timer on its left, a tap saves the recording as an inline audio player. An audio file can be uploaded instead; Esc/✕ restores the empty bullet. - Chrome's WebM recordings get their length measured, so the player shows the total time and seeking works. - With the own Whisper server "Transkribieren" turns the recording into the (searchable) caption. - Loaded files keep their Blob, as the CSP rightly blocks fetch() on blob: URLs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e8fa14a4cb
commit
3f5e3b2ac1
6 files changed
+378
-9
No files matched your search
@@ -23,11 +23,11 @@ export function mediaContent(kind: 'image' | 'audio', id: string, caption = ''):
|
||||
return `${kind === 'image' ? '!' : ''}[${safe}](media:${id})`;
|
||||
}
|
||||
|
||||
const objectURLs = new Map<string, Promise<string>>();
|
||||
// Loaded files, once per page. The Blob is kept as well: the CSP does not allow fetch() on blob: URLs.
|
||||
const loaded = new Map<string, Promise<{ blob: Blob; url: string }>>();
|
||||
|
||||
/** blob: URL of a file; loaded once per page. */
|
||||
export function mediaObjectURL(id: string): Promise<string> {
|
||||
let p = objectURLs.get(id);
|
||||
function loadMedia(id: string): Promise<{ blob: Blob; url: string }> {
|
||||
let p = loaded.get(id);
|
||||
if (!p) {
|
||||
p = (async () => {
|
||||
const token = getToken();
|
||||
@@ -35,14 +35,25 @@ export function mediaObjectURL(id: string): Promise<string> {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!res.ok) throw new Error(res.status === 404 ? 'Datei nicht gefunden' : `HTTP ${res.status}`);
|
||||
return URL.createObjectURL(await res.blob());
|
||||
const blob = await res.blob();
|
||||
return { blob, url: URL.createObjectURL(blob) };
|
||||
})();
|
||||
p.catch(() => objectURLs.delete(id)); // try again next time (e.g. was offline)
|
||||
objectURLs.set(id, p);
|
||||
p.catch(() => loaded.delete(id)); // try again next time (e.g. was offline)
|
||||
loaded.set(id, p);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/** blob: URL of a file for <img>/<audio>. */
|
||||
export function mediaObjectURL(id: string): Promise<string> {
|
||||
return loadMedia(id).then((m) => m.url);
|
||||
}
|
||||
|
||||
/** The file itself, e.g. to send a recording to the speech recognition. */
|
||||
export function mediaBlob(id: string): Promise<Blob> {
|
||||
return loadMedia(id).then((m) => m.blob);
|
||||
}
|
||||
|
||||
export interface UploadedMedia {
|
||||
id: string;
|
||||
kind: 'image' | 'audio';
|
||||
|
||||
Reference in new issue
Block a user