creates video voice memos from audio clips; with bluesky integration. trill.ptr.pet
at main 3.4 kB view raw
1import { AtprotoDid } from "@atcute/lexicons/syntax"; 2import { ReactiveMap } from "@solid-primitives/map"; 3import { 4 backgroundColor, 5 frameRate, 6 showProfilePicture, 7 showVisualizer, 8 useDominantColorAsBg, 9 autoTranscribe, 10} from "./settings"; 11import { getSessionClient } from "./oauth"; 12import { is } from "@atcute/lexicons"; 13import { AppBskyActorProfile } from "@atcute/bluesky"; 14import { isBlob } from "@atcute/lexicons/interfaces"; 15import { render } from "./render"; 16import { FastAverageColor } from "fast-average-color"; 17import { toaster } from "~/components/Toaster"; 18import { parseColor } from "@ark-ui/solid"; 19import { transcribe } from "./transcribe"; 20 21export type TaskState = { file: File } & ( 22 | { status: "processing" } 23 | { status: "error"; error: string } 24 | { status: "success"; result: Blob; altText?: string } 25); 26 27let _idCounter = 0; 28const generateId = () => _idCounter++; 29 30const fac = new FastAverageColor(); 31 32export const tasks = new ReactiveMap<number, TaskState>(); 33export const addTask = async ( 34 did: AtprotoDid | undefined, 35 file: File, 36 duration?: number, 37) => { 38 const id = generateId(); 39 tasks.set(id, { status: "processing", file }); 40 try { 41 let pfpUrl: string | undefined = undefined; 42 console.log(did, showProfilePicture.get()); 43 if (did && (showProfilePicture.get() ?? true)) { 44 const login = await getSessionClient(did); 45 const profileResult = await login.client.get( 46 "com.atproto.repo.getRecord", 47 { 48 params: { 49 collection: "app.bsky.actor.profile", 50 repo: did, 51 rkey: "self", 52 }, 53 }, 54 ); 55 if (!profileResult.ok) 56 throw `failed to fetch profile: ${profileResult.data.error}`; 57 if (!profileResult.data.value) throw `profile not found`; 58 const profile = profileResult.data.value; 59 if (!is(AppBskyActorProfile.mainSchema, profile)) 60 throw `invalid profile schema`; 61 if (profile.avatar && !isBlob(profile.avatar)) 62 throw `invalid profile avatar`; 63 pfpUrl = profile.avatar 64 ? `${login.pds!}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${profile.avatar.ref.$link}` 65 : undefined; 66 } 67 let bgColor = backgroundColor.get() ?? "#000000"; 68 if (pfpUrl && (useDominantColorAsBg.get() ?? true)) { 69 try { 70 const dom = await fac.getColorAsync(pfpUrl); 71 const color = parseColor(dom.hex).toFormat("hsla"); 72 const [h, s, l] = color 73 .getChannels() 74 .map((chan) => color.getChannelValue(chan)); 75 bgColor = color.withChannelValue("lightness", l * 0.4).toString("hex"); 76 } catch (error) { 77 console.error(error); 78 toaster.create({ 79 title: "can't pick dominant color", 80 description: `error: ${error}`, 81 type: "error", 82 }); 83 } 84 } 85 const [result, altText] = await Promise.all([ 86 render(file, { 87 pfpUrl, 88 visualizer: showVisualizer.get() ?? true, 89 frameRate: frameRate.get() ?? 30, 90 bgColor, 91 duration, 92 }), 93 (autoTranscribe.get() ?? false) 94 ? transcribe(file) 95 : Promise.resolve(undefined), 96 ]); 97 tasks.set(id, { 98 file, 99 status: "success", 100 result, 101 altText, 102 }); 103 } catch (error) { 104 console.error(error); 105 tasks.set(id, { 106 file, 107 status: "error", 108 error: `failed to process audio: ${error}`, 109 }); 110 } 111};