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};