fuck it we do it live

This commit is contained in:
Jack Merrill 2025-05-07 22:34:04 -04:00
parent 7f9bdee7f4
commit be2968aabc
No known key found for this signature in database
GPG Key ID: FD574AFF96E99636
22 changed files with 152 additions and 1775 deletions

View File

@ -1,37 +0,0 @@
import { forgotPasswordAction } from "@/app/actions";
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import { SmtpMessage } from "../smtp-message";
export default async function ForgotPassword(props: {
searchParams: Promise<Message>;
}) {
const searchParams = await props.searchParams;
return (
<>
<form className="flex-1 flex flex-col w-full gap-2 text-foreground [&>input]:mb-6 min-w-64 max-w-64 mx-auto">
<div>
<h1 className="text-2xl font-medium">Reset Password</h1>
<p className="text-sm text-secondary-foreground">
Already have an account?{" "}
<Link className="text-primary underline" href="/sign-in">
Sign in
</Link>
</p>
</div>
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
<Label htmlFor="email">Email</Label>
<Input name="email" placeholder="you@example.com" required />
<SubmitButton formAction={forgotPasswordAction}>
Reset Password
</SubmitButton>
<FormMessage message={searchParams} />
</div>
</form>
<SmtpMessage />
</>
);
}

View File

@ -1,9 +0,0 @@
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="max-w-7xl flex flex-col gap-12 items-start">{children}</div>
);
}

View File

@ -1,44 +0,0 @@
import { signInAction } from "@/app/actions";
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
export default async function Login(props: { searchParams: Promise<Message> }) {
const searchParams = await props.searchParams;
return (
<form className="flex-1 flex flex-col min-w-64">
<h1 className="text-2xl font-medium">Sign in</h1>
<p className="text-sm text-foreground">
Don't have an account?{" "}
<Link className="text-foreground font-medium underline" href="/sign-up">
Sign up
</Link>
</p>
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
<Label htmlFor="email">Email</Label>
<Input name="email" placeholder="you@example.com" required />
<div className="flex justify-between items-center">
<Label htmlFor="password">Password</Label>
<Link
className="text-xs text-foreground underline"
href="/forgot-password"
>
Forgot Password?
</Link>
</div>
<Input
type="password"
name="password"
placeholder="Your password"
required
/>
<SubmitButton pendingText="Signing In..." formAction={signInAction}>
Sign in
</SubmitButton>
<FormMessage message={searchParams} />
</div>
</form>
);
}

View File

@ -1,51 +0,0 @@
import { signUpAction } from "@/app/actions";
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import { SmtpMessage } from "../smtp-message";
export default async function Signup(props: {
searchParams: Promise<Message>;
}) {
const searchParams = await props.searchParams;
if ("message" in searchParams) {
return (
<div className="w-full flex-1 flex items-center h-screen sm:max-w-md justify-center gap-2 p-4">
<FormMessage message={searchParams} />
</div>
);
}
return (
<>
<form className="flex flex-col min-w-64 max-w-64 mx-auto">
<h1 className="text-2xl font-medium">Sign up</h1>
<p className="text-sm text text-foreground">
Already have an account?{" "}
<Link className="text-primary font-medium underline" href="/sign-in">
Sign in
</Link>
</p>
<div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
<Label htmlFor="email">Email</Label>
<Input name="email" placeholder="you@example.com" required />
<Label htmlFor="password">Password</Label>
<Input
type="password"
name="password"
placeholder="Your password"
minLength={6}
required
/>
<SubmitButton formAction={signUpAction} pendingText="Signing up...">
Sign up
</SubmitButton>
<FormMessage message={searchParams} />
</div>
</form>
<SmtpMessage />
</>
);
}

View File

@ -1,25 +0,0 @@
import { ArrowUpRight, InfoIcon } from "lucide-react";
import Link from "next/link";
export function SmtpMessage() {
return (
<div className="bg-muted/50 px-5 py-3 border rounded-md flex gap-4">
<InfoIcon size={16} className="mt-0.5" />
<div className="flex flex-col gap-1">
<small className="text-sm text-secondary-foreground">
<strong> Note:</strong> Emails are rate limited. Enable Custom SMTP to
increase the rate limit.
</small>
<div>
<Link
href="https://supabase.com/docs/guides/auth/auth-smtp"
target="_blank"
className="text-primary/50 hover:text-primary flex items-center text-sm gap-1"
>
Learn more <ArrowUpRight size={14} />
</Link>
</div>
</div>
</div>
);
}

View File

@ -21,7 +21,7 @@ Do not return the Markdown as a code block, only as a raw string, without any ne
No data or information should ever be removed, it should only be processed and formatted.
There are in-text citations/references in the text, remove them from the text (**but most importantly, keep the reference number in the text. use a <sup></sup> tag**) and put them into an object where the key is the reference number and the value is the text. If any citations contain JSON-breaking characters, ensure they are properly escaped. This includes characters like double quotes, backslashes, and newlines.
There are in-text citations/references in the text, remove them from the text (**but most importantly, keep the reference number in the text. use a <sup></sup> tag**) and put them into an object where the key is the reference number and the value is the text. (**Note that there may be multiple citations, usually split by commas. Ensure these are added too.**) If any citations contain JSON-breaking characters, ensure they are properly escaped. This includes characters like double quotes, backslashes, and newlines.
The Markdown should be human-readable and well-formatted. The markdown string should properly sanitized and should not break a JSON parser when returned as the final format.
@ -263,7 +263,11 @@ export async function POST(req: NextRequest) {
],
});
const split = response.choices[0].message.content.split("---------");
const contentData = response.choices?.[0]?.message?.content;
const split =
typeof contentData === "string"
? contentData.split("---------")
: ["", "{}"];
const content = split[0].trim();
const citationsStr = split[1]?.trim() || "{}";
console.log("Citations string:", citationsStr);

View File

@ -19,7 +19,11 @@ import { redirect } from "next/navigation";
import { TTSProvider } from "@/components/TTSProvider";
import MarkdownRenderer from "@/components/MarkdownRenderer";
export default async function DocumentPage(props: { params: { id: string } }) {
export default async function DocumentPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const supabase = await createClient();
const {
@ -35,7 +39,7 @@ export default async function DocumentPage(props: { params: { id: string } }) {
return redirect("/login");
}
const { id } = await props.params;
const { id } = await params;
// Fetch the document details based on the ID from params
const { data: document, error } = await supabase

View File

@ -73,9 +73,6 @@ export default async function Page() {
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="ml-auto px-3">
<NavActions />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 px-4 py-10">
<div className="bg-muted/50 mx-auto h-24 w-full max-w-3xl rounded-xl" />

BIN
bun.lockb

Binary file not shown.

View File

@ -10,6 +10,7 @@ import rehypeRaw from "rehype-raw";
import { useTTS } from "./TTSProvider";
import rehypeHighlight from "@/lib/utils";
import { Database } from "@/utils/supabase/types";
import remarkGfm from "remark-gfm";
// Utility to escape regex special characters:
function escapeRegExp(text: string) {
@ -18,7 +19,14 @@ function escapeRegExp(text: string) {
export type OCRData = {
index: number;
images: string[];
images: {
id: string;
topLeftX: number;
topLeftY: number;
bottomRightX: number;
bottomRightY: number;
imageBase64: string;
}[];
markdown: string;
citations: {
text: string;
@ -54,13 +62,22 @@ export default function MarkdownRenderer({
const ocr = document?.ocr_data as OCRData[];
const rawContent = ocr.map((page) => page.markdown).join("\n") || "";
// Join all markdown content from the OCR data into a single string. Add page separators.
const rawContent = ocr
.map((page) => page.markdown)
.map((page, index) => {
const pageIndex = index + 1; // 1-based index for pages
const pageSeparator = `\n\n###### Page ${pageIndex}\n\n`;
return `${page}${pageSeparator}`;
})
.join("\n\n");
const citations: {
text: string;
page: number;
index: string;
number: number;
inTextNumber: string;
}[] = [];
const totalPages = ocr.length;
const totalSentences = sentences.length;
@ -75,6 +92,7 @@ export default function MarkdownRenderer({
page: page.index,
index: (totalCitations + index).toString(), // unique index across all pages
number: totalCitations + index + 1, // 1-based numbering
inTextNumber: citation.number,
});
});
});
@ -92,16 +110,61 @@ export default function MarkdownRenderer({
h4: ({ node, ...props }) => (
<h4 className="text-lg font-bold mb-2 text-gray-300" {...props} />
),
h6: ({ node, ...props }) => {
const text = props.children!.toString().split(" ")[1];
const pageIndex = parseInt(text) - 1; // Convert to 0-based index
const page = ocr[pageIndex];
return (
<div className="relative flex py-5 items-center">
<div className="flex-grow border-t border-slate-800"></div>
<span className="flex-shrink mx-4 text-gray-400">
Page {pageIndex + 1} of {totalPages}
</span>
<div className="flex-grow border-t border-slate-800"></div>
</div>
);
},
p: ({ node, ...props }) => (
<p className="leading-7 text-gray-200" {...props} />
<p className="leading-7 text-gray-200 mb-3" {...props} />
),
img: ({ node, ...props }) => (
img: ({ node, ...props }) => {
const { src, alt } = props as any;
const pageIndex = ocr.findIndex((p) =>
p.images.find((image) => image.id === src)
);
if (pageIndex === -1) return null; // Handle the case where the page is not found
const page = ocr.find((p) => p.index === pageIndex);
if (!page) return null; // Handle the case where the page is not found
const img = page.images.find((image) => {
if (image.id === src) {
return true;
}
});
if (!img) return null; // Handle the case where the image is not found
// Calculate the width and height based on the image dimensions and DPI
const dpi = page.dimensions.dpi;
const width = ((img.bottomRightX - img.topLeftX) / dpi) * 96; // Convert to pixels
const height = ((img.bottomRightY - img.topLeftY) / dpi) * 96; // Convert to pixels
return (
<div className="w-full flex justify-center mb-4">
<img
className="rounded-lg shadow-sm"
style={{ maxWidth: "100%", height: "auto" }}
{...props}
data-image
src={img.imageBase64}
alt={alt}
width={width}
height={height}
/>
),
</div>
);
},
a: ({ node, ...props }) => (
<a className="text-blue-400 hover:underline" {...props} />
),
@ -122,6 +185,10 @@ export default function MarkdownRenderer({
),
sup: ({ node, ...props }) => {
// Check if the text contains a reference number
if (!props.children) {
return <sup {...props} />;
}
const text = props.children!.toString();
const referenceNumber = text;
@ -139,7 +206,6 @@ export default function MarkdownRenderer({
}
return (
// TODO: get the references from the document and display them in a popover
<Popover>
<PopoverTrigger asChild>
<sup
@ -150,11 +216,43 @@ export default function MarkdownRenderer({
<PopoverContent className="w-auto max-w-3xl bg-gray-900 overflow-hidden rounded-lg p-0">
<div className="p-4">
<p>{citation.text}</p>
<p className="text-sm text-gray-500">
Page {citation.page}, Reference {citation.inTextNumber}
</p>
</div>
</PopoverContent>
</Popover>
);
},
table: ({ node, ...props }) => (
<div className="relative overflow-x-auto shadow-md sm:rounded-lg">
<table
{...props}
className="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400"
/>
</div>
),
thead: ({ node, ...props }) => (
<thead
{...props}
className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"
/>
),
tbody: ({ node, ...props }) => <tbody {...props} />,
tr: ({ node, ...props }) => (
<tr
{...props}
className="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700 border-gray-200"
/>
),
th: ({ node, ...props }) => (
<th
{...props}
scope="col"
className="px-6 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white"
/>
),
td: ({ node, ...props }) => <td {...props} className="px-6 py-4" />,
};
return (
@ -162,6 +260,7 @@ export default function MarkdownRenderer({
children={rawContent}
components={components}
rehypePlugins={rehypePlugins}
remarkPlugins={[remarkGfm]}
/>
);
}

View File

@ -85,6 +85,8 @@ export const TTSProvider = ({
audioCache.current.set(i, audioUrl); // Cache the audio URL
} catch (error) {
console.error(`Error preloading audio for sentence ${i}:`, error);
} finally {
setProcessing((prev) => prev.filter((item) => item !== i)); // Remove from processing
}
}
}
@ -112,6 +114,7 @@ export const TTSProvider = ({
}
const playSentence = async (index: number) => {
if (index === currentSentence) return; // Prevent redundant updates
setCurrentSentence(index);
const sentence = removeMarkdown(sentences[index]);
@ -142,13 +145,12 @@ export const TTSProvider = ({
playSentence(index);
};
let shouldContinue = true;
const playInOrder = async (index: number) => {
if (index < 0 || index >= sentences.length) return;
setCurrentSentence(index);
// Introduce a flag to track whether playback should continue
let shouldContinue = true;
for (let i = index; i < sentences.length; i++) {
if (!shouldContinue) {
console.log("Playback stopped or paused.");
@ -158,14 +160,13 @@ export const TTSProvider = ({
console.log("Playing sentence:", i, sentences[i]);
try {
await playSentence(i);
preloadAudio(i + 1); // Preload the next sentence after playing
await preloadAudio(i + 1); // Preload the next sentence after playing
} catch (error) {
console.error("Error playing sentence:", error);
break; // Stop playback on error
}
}
// Reset the playback state when done
setStatus("ready");
};
@ -198,6 +199,23 @@ export const TTSProvider = ({
preloadAudio(currentSentence);
}, [currentSentence]);
useEffect(() => {
if (audioRef.current) {
const handleEnded = () => {
console.log("Audio playback ended.");
if (status === "running") {
playInOrder(currentSentence + 1); // Play the next sentence
}
};
audioRef.current.addEventListener("ended", handleEnded);
return () => {
audioRef.current?.removeEventListener("ended", handleEnded);
};
}
}, [currentSentence, status]);
const value: TTSContextType = {
sentences,
currentSentence,

View File

@ -39,7 +39,7 @@ export default function UploadZone({ user }: { user?: { id: string } }) {
},
});
eventSource.addEventListener("status", (event) => {
eventSource.addEventListener("status", (event: any) => {
const data = JSON.parse(event.data);
console.log("Status Event:", data);
supabase.auth.setSession;
@ -47,7 +47,7 @@ export default function UploadZone({ user }: { user?: { id: string } }) {
setStatus(data.message);
});
eventSource.addEventListener("error", (event) => {
eventSource.addEventListener("error", (event: any) => {
console.error("SSE Error:", event);
toast.error("An error occurred while processing the document", {
description: event.data || "Unknown error",
@ -56,7 +56,7 @@ export default function UploadZone({ user }: { user?: { id: string } }) {
eventSource.close();
});
eventSource.addEventListener("complete", (event) => {
eventSource.addEventListener("complete", (event: any) => {
const data = JSON.parse(event.data);
console.log("Processing Complete:", data);
toast.success("Document processing complete!");

View File

@ -9,6 +9,7 @@ export function cn(...inputs: ClassValue[]) {
export async function detectWebGPU() {
try {
// @ts-ignore
const adapter = await navigator.gpu.requestAdapter();
return !!adapter;
} catch (e) {

View File

@ -41,6 +41,7 @@
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.1",
"remark-html": "^16.0.1",
"remove-markdown": "^0.6.0",
"sonner": "^2.0.3",

8
supabase/.gitignore vendored
View File

@ -1,8 +0,0 @@
# Supabase
.branches
.temp
# dotenvx
.env.keys
.env.local
.env.*.local

View File

@ -1,319 +0,0 @@
# For detailed configuration reference documentation, visit:
# https://supabase.com/docs/guides/local-development/cli/config
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "neuro-read"
[api]
enabled = true
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"]
# Extra schemas to add to the search_path of every request.
extra_search_path = ["public", "extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000
[api.tls]
# Enable HTTPS endpoints locally using a self-signed certificate.
enabled = false
[db]
# Port to use for the local database URL.
port = 54322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 15
[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
# How many server connections to allow per user/database pair.
default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100
# [db.vault]
# secret_key = "env(SECRET_VALUE)"
[db.migrations]
# Specifies an ordered list of schema files that describe your database.
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
schema_paths = []
[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed.sql"]
[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096
[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[inbucket]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326
# admin_email = "admin@email.com"
# sender_name = "Admin"
[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"
# Image transformation API is available to Supabase Pro plan.
# [storage.image_transformation]
# enabled = true
# Uncomment to configure local storage buckets
# [storage.buckets.images]
# public = false
# file_size_limit = "50MiB"
# allowed_mime_types = ["image/png", "image/jpeg"]
# objects_path = "./images"
[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
# Requires enable_refresh_token_rotation = true.
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow anonymous sign-ins to your project.
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
minimum_password_length = 6
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
password_requirements = ""
[auth.rate_limit]
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
email_sent = 2
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
sms_sent = 30
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
anonymous_users = 30
# Number of sessions that can be refreshed in a 5 minute interval per IP address.
token_refresh = 150
# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
sign_in_sign_ups = 30
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
token_verifications = 30
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
# [auth.captcha]
# enabled = true
# provider = "hcaptcha"
# secret = ""
[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
# Number of characters used in the email OTP.
otp_length = 6
# Number of seconds before the email OTP expires (defaults to 1 hour).
otp_expiry = 3600
# Use a production-ready SMTP server
# [auth.email.smtp]
# enabled = true
# host = "smtp.sendgrid.net"
# port = 587
# user = "apikey"
# pass = "env(SENDGRID_API_KEY)"
# admin_email = "admin@email.com"
# sender_name = "Admin"
# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"
[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = false
# If enabled, users need to confirm their phone number before signing in.
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ .Code }}"
# Controls the minimum amount of time that must pass before sending another sms otp.
max_frequency = "5s"
# Use pre-defined map of phone number to OTP for testing.
# [auth.sms.test_otp]
# 4152127777 = "123456"
# Configure logged in session timeouts.
# [auth.sessions]
# Force log out after the specified duration.
# timebox = "24h"
# Force log out if the user has been inactive longer than the specified duration.
# inactivity_timeout = "8h"
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
# Multi-factor-authentication is available to Supabase Pro plan.
[auth.mfa]
# Control how many MFA factors can be enrolled at once per user.
max_enrolled_factors = 10
# Control MFA via App Authenticator (TOTP)
[auth.mfa.totp]
enroll_enabled = false
verify_enabled = false
# Configure MFA via Phone Messaging
[auth.mfa.phone]
enroll_enabled = false
verify_enabled = false
otp_length = 6
template = "Your code is {{ .Code }}"
max_frequency = "5s"
# Configure MFA via WebAuthn
# [auth.mfa.web_authn]
# enroll_enabled = true
# verify_enabled = true
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
# Overrides the default auth redirectUrl.
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
[auth.third_party.firebase]
enabled = false
# project_id = "my-firebase-project"
# Use Auth0 as a third-party provider alongside Supabase Auth.
[auth.third_party.auth0]
enabled = false
# tenant = "my-auth0-tenant"
# tenant_region = "us"
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
[auth.third_party.aws_cognito]
enabled = false
# user_pool_id = "my-user-pool-id"
# user_pool_region = "us-east-1"
# Use Clerk as a third-party provider alongside Supabase Auth.
[auth.third_party.clerk]
enabled = false
# Obtain from https://clerk.com/setup/supabase
# domain = "example.clerk.accounts.dev"
[edge_runtime]
enabled = true
# Configure one of the supported request policies: `oneshot`, `per_worker`.
# Use `oneshot` for hot reload, or `per_worker` for load testing.
policy = "oneshot"
# Port to attach the Chrome inspector for debugging edge functions.
inspector_port = 8083
# The Deno major version to use.
deno_version = 1
# [edge_runtime.secrets]
# secret_key = "env(SECRET_VALUE)"
[analytics]
enabled = true
port = 54327
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"
# Experimental features may be deprecated any time
[experimental]
# Configures Postgres storage engine to use OrioleDB (S3)
orioledb_version = ""
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
s3_host = "env(S3_HOST)"
# Configures S3 bucket region, eg. us-east-1
s3_region = "env(S3_REGION)"
# Configures AWS_ACCESS_KEY_ID for S3 bucket
s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"
[functions.generate-tts]
enabled = true
verify_jwt = true
import_map = "./functions/generate-tts/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
entrypoint = "./functions/generate-tts/index.ts"
# Specifies static files to be bundled with the function. Supports glob patterns.
# For example, if you want to serve static HTML pages in your function:
# static_files = [ "./functions/generate-tts/*.html" ]

View File

@ -1,3 +0,0 @@
# Configuration for private npm package dependencies
# For more information on using private registries with Edge Functions, see:
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries

View File

@ -1,3 +0,0 @@
{
"imports": {}
}

View File

@ -1,664 +0,0 @@
{
"version": "4",
"specifiers": {
"jsr:@supabase/functions-js@*": "2.4.4",
"npm:@mistralai/mistralai@*": "1.6.0_zod@3.24.4",
"npm:@types/node@*": "22.12.0",
"npm:kokoro-js@*": "1.2.1",
"npm:kokoro-tts@*": "0.0.1-security",
"npm:openai@^4.52.5": "4.97.0",
"npm:p-limit@*": "6.2.0"
},
"jsr": {
"@supabase/functions-js@2.4.4": {
"integrity": "38456509a6e22fb116b118464cbb36357256f9048d3580632b63af91f63769f7",
"dependencies": [
"npm:openai"
]
}
},
"npm": {
"@emnapi/runtime@1.4.3": {
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
"dependencies": [
"tslib"
]
},
"@huggingface/jinja@0.4.1": {
"integrity": "sha512-3WXbMFaPkk03LRCM0z0sylmn8ddDm4ubjU7X+Hg4M2GOuMklwoGAFXp9V2keq7vltoB/c7McE5aHUVVddAewsw=="
},
"@huggingface/transformers@3.5.1": {
"integrity": "sha512-qWsPoJMBPYcrGuzRMVL//3dwcLXED9r+j+Hzw+hZpBfrMPdyq57ehNs7lew0D34Paj0DO0E0kZQjOZcIVN17hQ==",
"dependencies": [
"@huggingface/jinja",
"onnxruntime-node",
"onnxruntime-web",
"sharp"
]
},
"@img/sharp-darwin-arm64@0.34.1": {
"integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==",
"dependencies": [
"@img/sharp-libvips-darwin-arm64"
]
},
"@img/sharp-darwin-x64@0.34.1": {
"integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==",
"dependencies": [
"@img/sharp-libvips-darwin-x64"
]
},
"@img/sharp-libvips-darwin-arm64@1.1.0": {
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="
},
"@img/sharp-libvips-darwin-x64@1.1.0": {
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="
},
"@img/sharp-libvips-linux-arm64@1.1.0": {
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="
},
"@img/sharp-libvips-linux-arm@1.1.0": {
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="
},
"@img/sharp-libvips-linux-ppc64@1.1.0": {
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="
},
"@img/sharp-libvips-linux-s390x@1.1.0": {
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="
},
"@img/sharp-libvips-linux-x64@1.1.0": {
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="
},
"@img/sharp-libvips-linuxmusl-arm64@1.1.0": {
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="
},
"@img/sharp-libvips-linuxmusl-x64@1.1.0": {
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="
},
"@img/sharp-linux-arm64@0.34.1": {
"integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==",
"dependencies": [
"@img/sharp-libvips-linux-arm64"
]
},
"@img/sharp-linux-arm@0.34.1": {
"integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==",
"dependencies": [
"@img/sharp-libvips-linux-arm"
]
},
"@img/sharp-linux-s390x@0.34.1": {
"integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==",
"dependencies": [
"@img/sharp-libvips-linux-s390x"
]
},
"@img/sharp-linux-x64@0.34.1": {
"integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==",
"dependencies": [
"@img/sharp-libvips-linux-x64"
]
},
"@img/sharp-linuxmusl-arm64@0.34.1": {
"integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==",
"dependencies": [
"@img/sharp-libvips-linuxmusl-arm64"
]
},
"@img/sharp-linuxmusl-x64@0.34.1": {
"integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==",
"dependencies": [
"@img/sharp-libvips-linuxmusl-x64"
]
},
"@img/sharp-wasm32@0.34.1": {
"integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==",
"dependencies": [
"@emnapi/runtime"
]
},
"@img/sharp-win32-ia32@0.34.1": {
"integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw=="
},
"@img/sharp-win32-x64@0.34.1": {
"integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="
},
"@isaacs/fs-minipass@4.0.1": {
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"dependencies": [
"minipass"
]
},
"@mistralai/mistralai@1.6.0_zod@3.24.4": {
"integrity": "sha512-PQwGV3+n7FbE7Dp3Vnd8DAa3ffx6WuVV966Gfmf4QvzwcO3Mvxpz0SnJ/PjaZcsCwApBCZpNyQzvarAKEQLKeQ==",
"dependencies": [
"zod",
"zod-to-json-schema"
]
},
"@protobufjs/aspromise@1.1.2": {
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"@protobufjs/base64@1.1.2": {
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"@protobufjs/codegen@2.0.4": {
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"@protobufjs/eventemitter@1.1.0": {
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"@protobufjs/fetch@1.1.0": {
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"dependencies": [
"@protobufjs/aspromise",
"@protobufjs/inquire"
]
},
"@protobufjs/float@1.0.2": {
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"@protobufjs/inquire@1.1.0": {
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"@protobufjs/path@1.1.2": {
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"@protobufjs/pool@1.1.0": {
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"@protobufjs/utf8@1.1.0": {
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@types/node-fetch@2.6.12": {
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"dependencies": [
"@types/node@22.12.0",
"form-data"
]
},
"@types/node@18.19.87": {
"integrity": "sha512-OIAAu6ypnVZHmsHCeJ+7CCSub38QNBS9uceMQeg7K5Ur0Jr+wG9wEOEvvMbhp09pxD5czIUy/jND7s7Tb6Nw7A==",
"dependencies": [
"undici-types@5.26.5"
]
},
"@types/node@22.12.0": {
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
"dependencies": [
"undici-types@6.20.0"
]
},
"abort-controller@3.0.0": {
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": [
"event-target-shim"
]
},
"agentkeepalive@4.6.0": {
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"dependencies": [
"humanize-ms"
]
},
"asynckit@0.4.0": {
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"boolean@3.2.0": {
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="
},
"call-bind-apply-helpers@1.0.2": {
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": [
"es-errors",
"function-bind"
]
},
"chownr@3.0.0": {
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="
},
"color-convert@2.0.1": {
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": [
"color-name"
]
},
"color-name@1.1.4": {
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"color-string@1.9.1": {
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": [
"color-name",
"simple-swizzle"
]
},
"color@4.2.3": {
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": [
"color-convert",
"color-string"
]
},
"combined-stream@1.0.8": {
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": [
"delayed-stream"
]
},
"define-data-property@1.1.4": {
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": [
"es-define-property",
"es-errors",
"gopd"
]
},
"define-properties@1.2.1": {
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dependencies": [
"define-data-property",
"has-property-descriptors",
"object-keys"
]
},
"delayed-stream@1.0.0": {
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"detect-libc@2.0.4": {
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="
},
"detect-node@2.1.0": {
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
},
"dunder-proto@1.0.1": {
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": [
"call-bind-apply-helpers",
"es-errors",
"gopd"
]
},
"es-define-property@1.0.1": {
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors@1.3.0": {
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-object-atoms@1.1.1": {
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": [
"es-errors"
]
},
"es-set-tostringtag@2.1.0": {
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": [
"es-errors",
"get-intrinsic",
"has-tostringtag",
"hasown"
]
},
"es6-error@4.1.1": {
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
},
"escape-string-regexp@4.0.0": {
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"event-target-shim@5.0.1": {
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"flatbuffers@25.2.10": {
"integrity": "sha512-7JlN9ZvLDG1McO3kbX0k4v+SUAg48L1rIwEvN6ZQl/eCtgJz9UylTMzE9wrmYrcorgxm3CX/3T/w5VAub99UUw=="
},
"form-data-encoder@1.7.2": {
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
},
"form-data@4.0.2": {
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dependencies": [
"asynckit",
"combined-stream",
"es-set-tostringtag",
"mime-types"
]
},
"formdata-node@4.4.1": {
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"dependencies": [
"node-domexception",
"web-streams-polyfill"
]
},
"function-bind@1.1.2": {
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"get-intrinsic@1.3.0": {
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": [
"call-bind-apply-helpers",
"es-define-property",
"es-errors",
"es-object-atoms",
"function-bind",
"get-proto",
"gopd",
"has-symbols",
"hasown",
"math-intrinsics"
]
},
"get-proto@1.0.1": {
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": [
"dunder-proto",
"es-object-atoms"
]
},
"global-agent@3.0.0": {
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"dependencies": [
"boolean",
"es6-error",
"matcher",
"roarr",
"semver",
"serialize-error"
]
},
"globalthis@1.0.4": {
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dependencies": [
"define-properties",
"gopd"
]
},
"gopd@1.2.0": {
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"guid-typescript@1.0.9": {
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="
},
"has-property-descriptors@1.0.2": {
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": [
"es-define-property"
]
},
"has-symbols@1.1.0": {
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"has-tostringtag@1.0.2": {
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": [
"has-symbols"
]
},
"hasown@2.0.2": {
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": [
"function-bind"
]
},
"humanize-ms@1.2.1": {
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dependencies": [
"ms"
]
},
"is-arrayish@0.3.2": {
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"json-stringify-safe@5.0.1": {
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
},
"kokoro-js@1.2.1": {
"integrity": "sha512-oq0HZJWis3t8lERkMJh84WLU86dpYD0EuBPtqYnLlQzyFP1OkyBRDcweAqCfhNOpltyN9j/azp1H6uuC47gShw==",
"dependencies": [
"@huggingface/transformers",
"phonemizer"
]
},
"kokoro-tts@0.0.1-security": {
"integrity": "sha512-VK7ILDqP0J88kiXdFrq7ymugipw42xLGmAU3kqpyFwJKlBqcsiTXVbvdTtehwYhYPw5QKJPmN2j7tD6fhPe6qw=="
},
"long@5.3.2": {
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
},
"matcher@3.0.0": {
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"dependencies": [
"escape-string-regexp"
]
},
"math-intrinsics@1.1.0": {
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"mime-db@1.52.0": {
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types@2.1.35": {
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": [
"mime-db"
]
},
"minipass@7.1.2": {
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
},
"minizlib@3.0.2": {
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"dependencies": [
"minipass"
]
},
"mkdirp@3.0.1": {
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node-domexception@1.0.0": {
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
},
"node-fetch@2.7.0": {
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": [
"whatwg-url"
]
},
"object-keys@1.1.1": {
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"onnxruntime-common@1.21.0": {
"integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ=="
},
"onnxruntime-common@1.22.0-dev.20250409-89f8206ba4": {
"integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ=="
},
"onnxruntime-node@1.21.0": {
"integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==",
"dependencies": [
"global-agent",
"onnxruntime-common@1.21.0",
"tar"
]
},
"onnxruntime-web@1.22.0-dev.20250409-89f8206ba4": {
"integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==",
"dependencies": [
"flatbuffers",
"guid-typescript",
"long",
"onnxruntime-common@1.22.0-dev.20250409-89f8206ba4",
"platform",
"protobufjs"
]
},
"openai@4.97.0": {
"integrity": "sha512-LRoiy0zvEf819ZUEJhgfV8PfsE8G5WpQi4AwA1uCV8SKvvtXQkoWUFkepD6plqyJQRghy2+AEPQ07FrJFKHZ9Q==",
"dependencies": [
"@types/node@18.19.87",
"@types/node-fetch",
"abort-controller",
"agentkeepalive",
"form-data-encoder",
"formdata-node",
"node-fetch"
]
},
"p-limit@6.2.0": {
"integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==",
"dependencies": [
"yocto-queue"
]
},
"phonemizer@1.2.1": {
"integrity": "sha512-v0KJ4mi2T4Q7eJQ0W15Xd4G9k4kICSXE8bpDeJ8jisL4RyJhNWsweKTOi88QXFc4r4LZlz5jVL5lCHhkpdT71A=="
},
"platform@1.3.6": {
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"protobufjs@7.5.0": {
"integrity": "sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA==",
"dependencies": [
"@protobufjs/aspromise",
"@protobufjs/base64",
"@protobufjs/codegen",
"@protobufjs/eventemitter",
"@protobufjs/fetch",
"@protobufjs/float",
"@protobufjs/inquire",
"@protobufjs/path",
"@protobufjs/pool",
"@protobufjs/utf8",
"@types/node@22.12.0",
"long"
]
},
"roarr@2.15.4": {
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"dependencies": [
"boolean",
"detect-node",
"globalthis",
"json-stringify-safe",
"semver-compare",
"sprintf-js"
]
},
"semver-compare@1.0.0": {
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="
},
"semver@7.7.1": {
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="
},
"serialize-error@7.0.1": {
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"dependencies": [
"type-fest"
]
},
"sharp@0.34.1": {
"integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==",
"dependencies": [
"@img/sharp-darwin-arm64",
"@img/sharp-darwin-x64",
"@img/sharp-libvips-darwin-arm64",
"@img/sharp-libvips-darwin-x64",
"@img/sharp-libvips-linux-arm",
"@img/sharp-libvips-linux-arm64",
"@img/sharp-libvips-linux-ppc64",
"@img/sharp-libvips-linux-s390x",
"@img/sharp-libvips-linux-x64",
"@img/sharp-libvips-linuxmusl-arm64",
"@img/sharp-libvips-linuxmusl-x64",
"@img/sharp-linux-arm",
"@img/sharp-linux-arm64",
"@img/sharp-linux-s390x",
"@img/sharp-linux-x64",
"@img/sharp-linuxmusl-arm64",
"@img/sharp-linuxmusl-x64",
"@img/sharp-wasm32",
"@img/sharp-win32-ia32",
"@img/sharp-win32-x64",
"color",
"detect-libc",
"semver"
]
},
"simple-swizzle@0.2.2": {
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": [
"is-arrayish"
]
},
"sprintf-js@1.1.3": {
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
},
"tar@7.4.3": {
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"dependencies": [
"@isaacs/fs-minipass",
"chownr",
"minipass",
"minizlib",
"mkdirp",
"yallist"
]
},
"tr46@0.0.3": {
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"tslib@2.8.1": {
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"type-fest@0.13.1": {
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="
},
"undici-types@5.26.5": {
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"undici-types@6.20.0": {
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"web-streams-polyfill@4.0.0-beta.3": {
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="
},
"webidl-conversions@3.0.1": {
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"whatwg-url@5.0.0": {
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": [
"tr46",
"webidl-conversions"
]
},
"yallist@5.0.0": {
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="
},
"yocto-queue@1.2.1": {
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="
},
"zod-to-json-schema@3.24.5_zod@3.24.4": {
"integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
"dependencies": [
"zod"
]
},
"zod@3.24.4": {
"integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="
}
},
"remote": {
"https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.1/+esm": "fb0a2ba5088bbde44cddff091a0d1844147cd08c75c75985df70672c7151efd5",
"https://cdn.jsdelivr.net/npm/kokoro-js@1.2.1/+esm": "cf3368de4ad22103fe3083600aa9a1bb764d12a0795b2994110836fb5b9091e4",
"https://cdn.jsdelivr.net/npm/kokoro-js@1.2.1/dist/kokoro.js/+esm": "cf3368de4ad22103fe3083600aa9a1bb764d12a0795b2994110836fb5b9091e4",
"https://cdn.jsdelivr.net/npm/kokoro-js@1.2.1/dist/kokoro.web.js": "6067712fe4c43cdb36c762f860a5ab8d96ea1d9c8882466438eb9f3d0d20be8f",
"https://cdn.jsdelivr.net/npm/onnxruntime-common/+esm": "54aea20a2aaa80823ca7c15c084008e838fe74b61f3026d7d5e0b0187b6b9fcb",
"https://cdn.jsdelivr.net/npm/onnxruntime-web@1.22.0-dev.20250409-89f8206ba4/+esm": "53bf11d80531b341f46d4dc695a3d143d8b4c71d30e43905f9588db9804832db",
"https://cdn.jsdelivr.net/npm/phonemizer@1.2.1/+esm": "b8b4327641797c57274be04e4c82a561dd1e63ec7d37bcbb9820f8be1e332696"
}
}

View File

@ -1,72 +0,0 @@
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
// Import necessary modules
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { KokoroTTS } from "https://cdn.jsdelivr.net/npm/kokoro-js@1.2.1/dist/kokoro.js/+esm";
console.log("generate-tts function initialized");
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
// Handle preflight requests
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
},
});
}
try {
console.log("Received request for TTS generation");
// Parse the incoming request body
const { text, voice, index } = await req.json();
console.log("Parsed request body:", { text, voice, index });
// Validate the input
if (!text || !voice) {
return new Response(
JSON.stringify({ error: "Missing required parameters: text or voice" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
console.log(`Generating TTS for text: "${text}" with voice: "${voice}"`);
// Initialize KokoroTTS
const model_id = "onnx-community/Kokoro-82M-v1.0-ONNX";
const tts = await KokoroTTS.from_pretrained(model_id, {
dtype: "fp32",
});
// Generate the speech audio
const audio = await tts.generate(text, {
voice,
});
const arrayBuffer = await audio.toWav();
// audioUrl should be the base64 encoded audio blob
const audioUrl = `data:audio/wav;base64,${btoa(
String.fromCharCode(...new Uint8Array(arrayBuffer))
)}`;
console.log(`TTS generated successfully for index: ${index}`);
// Return the audio URL
return new Response(JSON.stringify({ audioUrl }), {
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error generating TTS:", error);
// Return an error response
return new Response(JSON.stringify({ error: "Failed to generate TTS" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
});

View File

@ -1,510 +0,0 @@
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "jsr:@supabase/supabase-js@2";
import { Mistral } from "npm:@mistralai/mistralai";
import pLimit from "npm:p-limit";
export const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
};
const apiKey = Deno.env.get("MISTRAL_API_KEY");
const client = new Mistral({
apiKey: apiKey,
});
const PROCESSING_PROMPT = `
You are a document processing AI. Your task is to process the Markdown text scanned from a document page and return it in a clean and structured format.
The textual page data should only be returned in valid Markdown format. Use proper headings and subheadings to structure the content. **Do not add headings if they do not exist in the original text.** If there is a title to the document, it should be the first heading.
Any images should be included.
Do not return the Markdown as a code block, only as a raw string, without any new lines.
No data or information should ever be removed, it should only be processed and formatted.
There are in-text citations/references in the text, remove them from the text (**but most importantly, keep the reference number in the text. use a <sup></sup> tag**) and put them into an object where the key is the reference number and the value is the text.
The Markdown should be human-readable and well-formatted. The markdown string should properly sanitized and should not break a JSON parser when returned as the final format.
Return the final result as a text object with the following structure (without code block formatting):
"""
<processed markdown text>
---------
{
"citations": [
{
"number": 1, // The number as it appears in the text
"text": "Citation text 1" // Ensure any JSON-breaking characters are properly escaped
},
{
"number": 2,
"text": "Citation text 2"
}
]
}
"""
Do not return the text object as a code block, only as a raw string.
`;
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
console.log("Handling OPTIONS request...");
return new Response(null, {
headers: {
...corsHeaders,
"Access-Control-Allow-Methods": "POST, OPTIONS",
},
});
}
if (req.method === "POST") {
console.log("Processing POST request...");
const { body, writable } = new TransformStream();
const writer = writable.getWriter();
// Set up the SSE response
const headers = new Headers({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
...corsHeaders,
});
let activeOperations = 0; // Track active operations
let streamClosed = false; // Track if the stream is closed
const sendEvent = async (event, data) => {
if (streamClosed) {
console.warn("Attempted to write to a closed stream.");
return;
}
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
console.log("Sending event:", message);
try {
activeOperations++;
await writer.write(new TextEncoder().encode(message));
} catch (error) {
console.error("Error writing to stream:", error);
} finally {
activeOperations--;
}
};
// Start streaming updates
sendEvent("status", {
message: "Initializing...",
});
try {
const supabase = createClient(
Deno.env.get("SUPABASE_URL"),
Deno.env.get("SUPABASE_ANON_KEY")
);
const supabaseServer = createClient(
Deno.env.get("SUPABASE_URL"),
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")
);
const formData = await req.formData();
const accessToken = formData.get("access_token");
const refreshToken = formData.get("refresh_token");
var reprocessing = false;
var uuid = crypto.randomUUID();
const {
data: { user },
error: sessionError,
} = await supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken,
});
if (sessionError) {
console.error("Error setting session:", sessionError);
sendEvent("error", {
message: "Error setting session",
error: sessionError,
});
throw new Error("Setting session failed");
}
if (formData.has("id")) {
console.log("Reprocessing document...");
reprocessing = true;
console.log("File ID found in form data.");
sendEvent("status", {
message: "File ID found in form data.",
});
const docId = formData.get("id");
console.log("Document ID:", docId, formData);
const { data: documentData, error: documentError } = await supabase
.from("documents")
.select("*")
.eq("id", docId)
.single();
if (documentError) {
console.error("Error fetching document record:", documentError);
sendEvent("error", {
message: "Error fetching document record",
error: documentError,
});
throw new Error("Document record fetch failed");
}
if (documentData) {
await supabase
.from("documents")
.update({
is_processing: true,
})
.eq("id", documentData.id);
uuid = documentData.id;
} else {
console.error("Document record not found.");
sendEvent("error", {
message: "Document record not found",
});
throw new Error("Document record not found");
}
const { data: fileData, error: fileError } = await supabase.storage
.from("documents")
.download(`${user.id}/${uuid}.pdf`);
if (fileError) {
console.error("Error downloading file from storage:", fileError);
sendEvent("error", {
message: "Error downloading file from storage",
error: fileError,
});
throw new Error("File download failed");
}
console.log("File downloaded from storage:", fileData);
sendEvent("status", {
message: "File downloaded from storage",
fileData,
});
formData.set("file", fileData);
}
if (!formData.has("file")) {
console.error("File not found in form data.");
sendEvent("error", {
message: "File not found in form data",
});
throw new Error("File not found");
}
if (!formData.has("access_token") || !formData.has("refresh_token")) {
console.error("Access token or refresh token not found in form data.");
sendEvent("error", {
message: "Access token or refresh token not found in form data",
});
throw new Error("Tokens not found");
}
const file = formData.get("file") as File;
const fileName = file.name;
console.log("Generated UUID:", uuid);
sendEvent("status", {
message: "Generated UUID",
uuid,
});
console.log("Authenticated user:", user);
sendEvent("status", {
message: "Authenticated user",
user,
});
if (!reprocessing) {
const { data: storageData, error: storageError } =
await supabase.storage
.from("documents")
.upload(`${user.id}/${uuid}.pdf`, file);
if (storageError) {
console.error("Error uploading file to storage:", storageError);
sendEvent("error", {
message: "Error uploading file to storage",
error: storageError,
});
throw new Error("File upload failed");
}
console.log("File uploaded to storage:", storageData);
sendEvent("status", {
message: "File uploaded to storage",
storageData,
});
const { error: docError } = await supabase.from("documents").insert({
id: uuid,
file_name: file.name,
owner: user.id,
raw_file: storageData.id,
is_processing: true,
});
if (docError) {
console.error("Error inserting document record:", docError);
sendEvent("error", {
message: "Error inserting document record",
error: docError,
});
throw new Error("Document record insertion failed");
}
console.log("Document record inserted successfully.");
sendEvent("status", {
message: "Document record inserted successfully",
});
} else {
console.log("Reprocessing document...");
sendEvent("status", {
message: "Reprocessing document",
});
const { error: docError } = await supabase
.from("documents")
.update({
is_processing: true,
})
.eq("id", uuid);
if (docError) {
console.error("Error updating document record:", docError);
sendEvent("error", {
message: "Error updating document record",
error: docError,
});
throw new Error("Document record update failed");
}
console.log("Document record updated successfully.");
sendEvent("status", {
message: "Document record updated successfully",
});
}
console.log("Uploading file to Mistral...");
sendEvent("status", {
message: "Uploading file to Mistral...",
});
const uploaded_pdf = await client.files.upload({
file: {
fileName,
content: file,
},
purpose: "ocr",
});
console.log("File uploaded to Mistral:", uploaded_pdf);
sendEvent("status", {
message: "File uploaded to Mistral",
uploaded_pdf,
});
const signedUrl = await client.files.getSignedUrl({
fileId: uploaded_pdf.id,
});
console.log("Generated signed URL:", signedUrl);
sendEvent("status", {
message: "Generated signed URL",
signedUrl,
});
console.log("Processing OCR...");
sendEvent("status", {
message: "Processing OCR...",
});
const ocrResponse = await client.ocr.process({
model: "mistral-ocr-latest",
document: {
type: "document_url",
documentUrl: signedUrl.url,
},
});
console.log("OCR response received:", ocrResponse);
sendEvent("status", {
message: "OCR response received",
ocrResponse,
});
const limit = pLimit(2);
const promises = [];
for (const page of ocrResponse.pages) {
console.log("Processing page:", page.index);
sendEvent("status", {
message: `Processing page ${page.index}`,
});
const pagePromise = limit(async () => {
console.log(`Processing page ${page.index} with Mistral...`);
const response = await client.chat.complete({
model: "mistral-small-latest",
messages: [
{
role: "system",
content: [
{
type: "text",
text: PROCESSING_PROMPT,
},
],
},
{
role: "user",
content: [
{
type: "text",
text: page.markdown,
},
],
},
],
});
if (!response.choices) {
console.error("No choices in response for page:", page.index);
sendEvent("error", {
message: `No choices in response for page ${page.index}`,
});
return;
}
console.log("Response received for page:", page.index);
sendEvent("status", {
message: `Response received for page ${page.index}`,
});
const imageData = {};
if (page.images.length > 0) {
console.log(
`Processing ${page.images.length} images for page ${page.index}...`
);
sendEvent("status", {
message: `Processing images for page ${page.index}`,
});
for (const img of page.images) {
imageData[img.id] = img.imageBase64;
}
}
if (response.choices[0].message.content) {
// remove any potential code block formatting from the content
console.log(
`[${page.index}] ${response.choices[0].message.content}`
);
const split =
response.choices[0].message.content.split("---------");
const content = split[0].trim();
const citationsStr = split[1]?.trim() || "{}";
console.log(`[${page.index}] Citations: ${citationsStr}`);
const citations = JSON.parse(citationsStr).citations || {};
console.log("Generating Markdown for page:", page.index);
sendEvent("status", {
message: `Generating Markdown for page ${page.index}`,
});
const markdown = replaceImagesInMarkdown(content, imageData);
return {
...page,
markdown,
citations,
};
} else {
console.error("Message content is undefined for page:", page.index);
sendEvent("error", {
message: `Message content is undefined for page ${page.index}`,
});
}
});
promises.push(pagePromise);
}
console.log("Waiting for all pages to be processed...");
sendEvent("status", {
message: "Waiting for all pages to be processed...",
});
const results = await Promise.all(promises);
console.log("All pages processed. Results:", results);
sendEvent("status", {
message: "All pages processed",
results,
});
const sortedResults = results.sort((a, b) => a.index - b.index);
console.log("Sorted results:", sortedResults);
sendEvent("status", { message: "Sorted results", sortedResults });
const { data, error } = await supabase
.from("documents")
.update({
ocr_data: sortedResults,
is_processing: false,
})
.eq("id", uuid);
if (error) {
console.error("Error updating document record:", error);
sendEvent("error", {
message: "Error updating document record",
error,
});
throw new Error("Document record update failed");
}
console.log("Closing SSE stream...");
} catch (error) {
console.error("Error during processing:", error);
sendEvent("error", {
message: "Error during processing",
error,
});
} finally {
// Wait for all active operations to complete before closing the stream
const interval = setInterval(() => {
if (activeOperations === 0) {
clearInterval(interval);
streamClosed = true;
writer.close();
}
}, 100); // Check every 100ms
}
return new Response(body, {
headers,
});
}
console.error("Method not allowed:", req.method);
return new Response("Method not allowed", {
status: 405,
});
});
function replaceImagesInMarkdown(markdownStr, imagesDict) {
console.log("Replacing images in Markdown...");
for (const [imgName, base64Str] of Object.entries(imagesDict)) {
markdownStr = markdownStr.replace(
new RegExp(`!\\[${imgName}\\]\\(${imgName}\\)`, "g"),
`![${imgName}](${base64Str})`
);
}
console.log("Image replacement complete.");
return markdownStr;
}

View File

@ -1,7 +1,5 @@
import type { Config } from "tailwindcss";
const config = {
darkMode: ["class"],
darkMode: "class",
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
@ -75,6 +73,6 @@ const config = {
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;
};
export default config;