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