├── .prettierrc ├── public ├── og.jpg ├── icons │ ├── 1024.png │ └── 512.png ├── home-screen.png ├── square-logo.png ├── home-screen-desktop.png ├── new.svg ├── file.svg ├── logo.svg ├── stop.svg ├── twitter.svg ├── X.svg ├── site.webmanifest ├── pause.svg ├── recordings │ ├── blog.svg │ ├── summary.svg │ ├── list.svg │ ├── custom-prompt.svg │ ├── quick-note.svg │ └── email.svg ├── globe.svg ├── loading.svg ├── microphone.svg ├── copy.svg ├── key.svg ├── spinner.svg ├── spark.svg ├── back.svg ├── uploadWhite.svg ├── github.svg ├── sparkFull.svg ├── upload.svg ├── microphoneFull.svg ├── togetherai.svg └── command.svg ├── app ├── favicon.ico ├── api │ ├── s3-upload │ │ └── route.ts │ ├── trpc │ │ └── [trpc] │ │ │ └── route.ts │ ├── validate-key │ │ └── route.ts │ └── transform │ │ └── route.ts ├── page.tsx ├── whispers │ ├── [id] │ │ ├── page.tsx │ │ └── TranscriptionPageClient.tsx │ └── page.tsx ├── layout.tsx └── globals.css ├── postcss.config.mjs ├── next.config.ts ├── proxy.ts ├── components ├── RecordingMinutesLeft.tsx ├── whisper-page │ └── LoadingSection.tsx ├── ui │ ├── sonner.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── button.tsx │ ├── AutoSizeTextArea.tsx │ ├── dialog.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── hooks │ ├── useLimits.ts │ ├── useLocalStorage.ts │ ├── useAudioRecording.ts │ └── ModalCustomApiKey.tsx ├── Footer.tsx ├── TogetherApiKeyProvider.tsx ├── RecordingBasics.tsx ├── TransformDropdown.tsx ├── landing-page.tsx ├── AudioWaveform.tsx ├── Header.tsx ├── CustomMarkdown.tsx ├── transcription-view.tsx ├── UploadModal.tsx ├── RecordingModal.tsx └── dashboard.tsx ├── components.json ├── trpc ├── routers │ ├── _app.ts │ ├── limit.ts │ └── whisper.ts ├── query-client.ts ├── init.ts ├── server.tsx └── client.tsx ├── .example.env ├── .gitignore ├── tsconfig.json ├── LICENSE ├── lib ├── apiClients.ts ├── utils.ts └── limits.ts ├── README.md ├── prisma └── schema.prisma └── package.json /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false 3 | } 4 | -------------------------------------------------------------------------------- /public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/whisper/HEAD/public/og.jpg -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/whisper/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/icons/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/whisper/HEAD/public/icons/1024.png -------------------------------------------------------------------------------- /public/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/whisper/HEAD/public/icons/512.png -------------------------------------------------------------------------------- /public/home-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/whisper/HEAD/public/home-screen.png -------------------------------------------------------------------------------- /public/square-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/whisper/HEAD/public/square-logo.png -------------------------------------------------------------------------------- /app/api/s3-upload/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/s3-upload/route.js 2 | export { POST } from "next-s3-upload/route"; 3 | -------------------------------------------------------------------------------- /public/home-screen-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/whisper/HEAD/public/home-screen-desktop.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = {}; 4 | 5 | export default nextConfig; 6 | -------------------------------------------------------------------------------- /public/new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { LandingPage } from "@/components/landing-page"; 4 | 5 | export interface Transcription { 6 | id: string; 7 | title: string; 8 | content: string; 9 | preview: string; 10 | timestamp: string; 11 | duration?: string; 12 | } 13 | 14 | export default function Home() { 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /proxy.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware } from "@clerk/nextjs/server"; 2 | 3 | export default clerkMiddleware(); 4 | 5 | export const config = { 6 | matcher: [ 7 | // Skip Next.js internals and all static files, unless found in search params 8 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", 9 | // Always run for API routes 10 | "/(api|trpc)(.*)", 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /components/RecordingMinutesLeft.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function RecordingMinutesLeft({ minutesLeft }: { minutesLeft: number }) { 4 | return ( 5 |
6 | Recording minutes left: 7 | 8 | {minutesLeft === Infinity ? "∞" : minutesLeft} 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /trpc/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { t } from "../init"; 2 | import { z } from "zod"; 3 | import { whisperRouter } from "./whisper"; 4 | import { limitRouter } from "./limit"; 5 | 6 | export const appRouter = t.router({ 7 | hello: t.procedure 8 | .input(z.object({ text: z.string().optional() })) 9 | .query(({ input }) => { 10 | return { greeting: `Hello, ${input?.text ?? "world"}!` }; 11 | }), 12 | whisper: whisperRouter, 13 | limit: limitRouter, 14 | }); 15 | 16 | export type AppRouter = typeof appRouter; 17 | -------------------------------------------------------------------------------- /app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { appRouter } from "@/trpc/routers/_app"; 2 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 3 | import { createTRPCContext } from "@/trpc/init"; 4 | import type { NextRequest } from "next/server"; 5 | 6 | const handler = (req: NextRequest) => { 7 | return fetchRequestHandler({ 8 | endpoint: "/api/trpc", 9 | req, 10 | router: appRouter, 11 | createContext: () => createTRPCContext({ req }), 12 | }); 13 | }; 14 | 15 | export { handler as GET, handler as POST }; 16 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.example.env: -------------------------------------------------------------------------------- 1 | # API Keys 2 | TOGETHER_API_KEY=your_together_api_key 3 | 4 | # Clerk for Auth 5 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 6 | CLERK_SECRET_KEY= 7 | NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL=/whispers 8 | NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL=/whispers 9 | 10 | # S3 AWS Credentials to upload Audio Files 11 | S3_UPLOAD_KEY= 12 | S3_UPLOAD_SECRET= 13 | S3_UPLOAD_BUCKET= 14 | S3_UPLOAD_REGION= 15 | 16 | # Upstash Redis for rate limiting 17 | UPSTASH_REDIS_REST_URL= 18 | UPSTASH_REDIS_REST_TOKEN= 19 | 20 | # Neon for the postgres DB 21 | DATABASE_URL= 22 | -------------------------------------------------------------------------------- /public/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/X.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/whisper-page/LoadingSection.tsx: -------------------------------------------------------------------------------- 1 | export const LoadingSection = () => { 2 | return ( 3 |
4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "id": "usewhisper", 3 | "display": "standalone", 4 | "name": "Whisper", 5 | "short_name": "Whisper", 6 | "theme_color": "#ffffff", 7 | "background_color": "#ffffff", 8 | "description": "Convert your thoughts into text by voice with Whisper", 9 | "start_url": "/whisper", 10 | "icons": [ 11 | { 12 | "src": "/icons/512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "any maskable" 16 | }, 17 | { 18 | "src": "/icons/1024.png", 19 | "sizes": "1024x1024", 20 | "type": "image/png", 21 | "purpose": "any maskable" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /trpc/query-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultShouldDehydrateQuery, 3 | QueryClient, 4 | } from "@tanstack/react-query"; 5 | // import superjson from 'superjson'; // Uncomment if you use superjson 6 | 7 | export function makeQueryClient() { 8 | return new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | staleTime: 30 * 1000, 12 | }, 13 | dehydrate: { 14 | // serializeData: superjson.serialize, 15 | shouldDehydrateQuery: (query) => 16 | defaultShouldDehydrateQuery(query) || 17 | query.state.status === "pending", 18 | }, 19 | hydrate: { 20 | // deserializeData: superjson.deserialize, 21 | }, 22 | }, 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |