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