├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── atoms └── index.ts ├── components ├── dropzone.tsx ├── file-select.tsx ├── icons.tsx ├── key-popover.tsx ├── layout.tsx ├── main-nav.tsx ├── site-header.tsx ├── theme-toggle.tsx ├── transcription-loader.tsx └── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── hover-card.tsx │ ├── input.tsx │ ├── label.tsx │ ├── menubar.tsx │ ├── navigation-menu.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── slider.tsx │ ├── switch.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── toggle.tsx │ └── tooltip.tsx ├── config └── site.ts ├── hooks └── use-toast.ts ├── lib ├── openai.ts └── utils.ts ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ └── transcribe.ts ├── index.tsx ├── video.tsx └── youtube.tsx ├── postcss.config.js ├── prettier.config.js ├── public ├── favicon.ico ├── next.svg ├── robot-loading-animation.json ├── thirteen.svg └── vercel.svg ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.tsbuildinfo └── types └── nav.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | .cache 3 | public 4 | node_modules 5 | *.esm.js 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": ["tailwindcss"], 10 | "rules": { 11 | "@next/next/no-html-link-for-pages": "off", 12 | "react/jsx-key": "off", 13 | "tailwindcss/no-custom-classname": "off" 14 | }, 15 | "settings": { 16 | "tailwindcss": { 17 | "callees": ["cn"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .contentlayer 36 | .env 37 | 38 | .vscode/* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | cache 2 | .cache 3 | package.json 4 | package-lock.json 5 | public 6 | CHANGELOG.md 7 | .yarn 8 | dist 9 | node_modules 10 | .next 11 | build 12 | .contentlayer -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sully Omar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Videoscribe 2 | 3 | Transcribe your videos using OpenAI's whisper api. 4 | 5 | Code is free and 100% open source. You will need an api key from openai. 6 | 7 | ## Features 8 | 9 | - Youtube transcriber 10 | - Uploaded file transcriber 11 | 12 | ## Built with 13 | 14 | - TailwindCSS 15 | - Next13 16 | 17 | ## Components & UI 18 | 19 | This project was boostrapped using this [template](https://ui.shadcn.com/) 20 | 21 | ### Usage 22 | 23 | Simply add your api key, then select whether you want to transcribe a youtube video or your own file. Note: you will be charged based on OpenAI's pricing found [here](https://openai.com/pricing) 24 | 25 | ### Additional notes 26 | 27 | I tried to make this as simple as possible, so yes, there could be several improvements, specifically in making the components more reusable. 28 | 29 | ## License 30 | 31 | Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md). 32 | -------------------------------------------------------------------------------- /atoms/index.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from "jotai/utils"; 2 | 3 | export const apiKeyAtom = atomWithStorage("apiKey", ""); 4 | -------------------------------------------------------------------------------- /components/dropzone.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { useDropzone } from "react-dropzone"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | import { Icons } from "./icons"; 6 | import { Button } from "./ui/button"; 7 | 8 | interface DropzoneProps { 9 | files: File[]; 10 | setFiles: (files: File[]) => void; 11 | } 12 | 13 | const Dropzone = ({ setFiles, files }: DropzoneProps) => { 14 | const onDrop = useCallback( 15 | (acceptedFiles: File[]) => { 16 | setFiles(acceptedFiles); 17 | }, 18 | [setFiles] 19 | ); 20 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ 21 | onDrop, 22 | multiple: false, 23 | accept: { 24 | "video/mp4": [".mp4", ".m4a"], 25 | "video/mpeg": [".mpeg"], 26 | "audio/mpeg": [".mp3"], 27 | "audio/webm": [".webm"], 28 | "audio/wav": [".wav"], 29 | }, 30 | }); 31 | 32 | return ( 33 |
42 | 43 | 44 | 45 |

or drag and drop

46 |
47 | ); 48 | }; 49 | 50 | export default Dropzone; 51 | -------------------------------------------------------------------------------- /components/file-select.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Select, 3 | SelectContent, 4 | SelectItem, 5 | SelectTrigger, 6 | SelectValue, 7 | } from "@/components/ui/select"; 8 | 9 | export default function FileSelect({ 10 | format, 11 | setFileFormat, 12 | }: { 13 | format: string; 14 | setFileFormat: (format: string) => void; 15 | }) { 16 | return ( 17 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Laptop, 3 | LucideFileDown, 4 | LucideProps, 5 | Moon, 6 | Settings2Icon, 7 | SunMedium, 8 | Twitter, 9 | UploadCloud, 10 | type Icon as LucideIcon, 11 | } from "lucide-react"; 12 | 13 | export type Icon = LucideIcon; 14 | 15 | export const Icons = { 16 | sun: SunMedium, 17 | moon: Moon, 18 | laptop: Laptop, 19 | twitter: Twitter, 20 | upload: UploadCloud, 21 | download: LucideFileDown, 22 | settings: Settings2Icon, 23 | logo: (props: LucideProps) => ( 24 | 25 | 29 | 30 | ), 31 | gitHub: (props: LucideProps) => ( 32 | 33 | 37 | 38 | ), 39 | }; 40 | -------------------------------------------------------------------------------- /components/key-popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { apiKeyAtom } from "@/atoms"; 4 | import { useAtom } from "jotai"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Input } from "@/components/ui/input"; 8 | import { Label } from "@/components/ui/label"; 9 | import { 10 | Popover, 11 | PopoverContent, 12 | PopoverTrigger, 13 | } from "@/components/ui/popover"; 14 | 15 | export function KeyPopover() { 16 | const [apiKey, setApiKey] = useAtom(apiKeyAtom); 17 | 18 | return ( 19 | 20 | 21 | 30 | 31 | 32 |
33 |
34 |

OpenAI API Key

35 |

36 | Add your OpenAI API key to use the whisper API. 37 |

38 |
39 |
40 |
41 | 42 | { 48 | setApiKey(e.target.value); 49 | }} 50 | /> 51 |
52 |
53 |
54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /components/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SiteHeader } from "@/components/site-header" 2 | 3 | interface LayoutProps { 4 | children: React.ReactNode 5 | } 6 | 7 | export function Layout({ children }: LayoutProps) { 8 | return ( 9 | <> 10 | 11 |
{children}
12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import Link from "next/link" 3 | 4 | import { NavItem } from "@/types/nav" 5 | import { siteConfig } from "@/config/site" 6 | import { cn } from "@/lib/utils" 7 | import { Icons } from "@/components/icons" 8 | import { Button } from "@/components/ui/button" 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuLabel, 14 | DropdownMenuSeparator, 15 | DropdownMenuTrigger, 16 | } from "@/components/ui/dropdown-menu" 17 | 18 | interface MainNavProps { 19 | items?: NavItem[] 20 | } 21 | 22 | export function MainNav({ items }: MainNavProps) { 23 | return ( 24 |
25 | 26 | 27 | 28 | {siteConfig.name} 29 | 30 | 31 | {items?.length ? ( 32 | 49 | ) : null} 50 | 51 | 52 | 59 | 60 | 65 | 66 | 67 | {siteConfig.name} 68 | 69 | 70 | 71 | {items?.map( 72 | (item, index) => 73 | item.href && ( 74 | 75 | {item.title} 76 | 77 | ) 78 | )} 79 | 80 | 81 |
82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /components/site-header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { siteConfig } from "@/config/site"; 4 | import { Icons } from "@/components/icons"; 5 | import { KeyPopover } from "@/components/key-popover"; 6 | import { MainNav } from "@/components/main-nav"; 7 | import { ThemeToggle } from "@/components/theme-toggle"; 8 | import { buttonVariants } from "@/components/ui/button"; 9 | 10 | export function SiteHeader() { 11 | return ( 12 |
13 |
14 | 15 |
16 | 53 |
54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { useTheme } from "next-themes" 3 | 4 | import { Icons } from "@/components/icons" 5 | import { Button } from "@/components/ui/button" 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger, 11 | } from "@/components/ui/dropdown-menu" 12 | 13 | export function ThemeToggle() { 14 | const { setTheme } = useTheme() 15 | 16 | return ( 17 | 18 | 19 | 24 | 25 | 26 | setTheme("light")}> 27 | 28 | Light 29 | 30 | setTheme("dark")}> 31 | 32 | Dark 33 | 34 | setTheme("system")}> 35 | 36 | System 37 | 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /components/transcription-loader.tsx: -------------------------------------------------------------------------------- 1 | import { Player } from "@lottiefiles/react-lottie-player"; 2 | 3 | export function RobotLoader() { 4 | return ( 5 |
6 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { ChevronDown } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )); 24 | AccordionItem.displayName = "AccordionItem"; 25 | 26 | const AccordionTrigger = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, children, ...props }, ref) => ( 30 | 31 | svg]:rotate-180", 35 | className 36 | )} 37 | {...props} 38 | > 39 | {children} 40 | 41 | 42 | 43 | )); 44 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 45 | 46 | const AccordionContent = React.forwardRef< 47 | React.ElementRef, 48 | React.ComponentPropsWithoutRef 49 | >(({ className, children, ...props }, ref) => ( 50 | 58 |
{children}
59 |
60 | )); 61 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 62 | 63 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 64 | -------------------------------------------------------------------------------- /components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const AlertDialog = AlertDialogPrimitive.Root; 9 | 10 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 11 | 12 | const AlertDialogPortal = ({ 13 | className, 14 | children, 15 | ...props 16 | }: AlertDialogPrimitive.AlertDialogPortalProps) => ( 17 | 18 |
19 | {children} 20 |
21 |
22 | ); 23 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName; 24 | 25 | const AlertDialogOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, children, ...props }, ref) => ( 29 | 37 | )); 38 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 39 | 40 | const AlertDialogContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 45 | 46 | 55 | 56 | )); 57 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 58 | 59 | const AlertDialogHeader = ({ 60 | className, 61 | ...props 62 | }: React.HTMLAttributes) => ( 63 |
70 | ); 71 | AlertDialogHeader.displayName = "AlertDialogHeader"; 72 | 73 | const AlertDialogFooter = ({ 74 | className, 75 | ...props 76 | }: React.HTMLAttributes) => ( 77 |
84 | ); 85 | AlertDialogFooter.displayName = "AlertDialogFooter"; 86 | 87 | const AlertDialogTitle = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => ( 91 | 100 | )); 101 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 102 | 103 | const AlertDialogDescription = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )); 113 | AlertDialogDescription.displayName = 114 | AlertDialogPrimitive.Description.displayName; 115 | 116 | const AlertDialogAction = React.forwardRef< 117 | React.ElementRef, 118 | React.ComponentPropsWithoutRef 119 | >(({ className, ...props }, ref) => ( 120 | 128 | )); 129 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 130 | 131 | const AlertDialogCancel = React.forwardRef< 132 | React.ElementRef, 133 | React.ComponentPropsWithoutRef 134 | >(({ className, ...props }, ref) => ( 135 | 143 | )); 144 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 145 | 146 | export { 147 | AlertDialog, 148 | AlertDialogTrigger, 149 | AlertDialogContent, 150 | AlertDialogHeader, 151 | AlertDialogFooter, 152 | AlertDialogTitle, 153 | AlertDialogDescription, 154 | AlertDialogAction, 155 | AlertDialogCancel, 156 | }; 157 | -------------------------------------------------------------------------------- /components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { VariantProps, cva } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const buttonVariants = cva( 7 | "active:scale-95 inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900", 13 | destructive: 14 | "bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600", 15 | outline: 16 | "bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100", 17 | subtle: 18 | "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100", 19 | ghost: 20 | "bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent", 21 | link: "bg-transparent dark:bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent", 22 | }, 23 | size: { 24 | default: "h-10 py-2 px-4", 25 | sm: "h-9 px-2 rounded-md", 26 | lg: "h-11 px-8 rounded-md", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps {} 39 | 40 | const Button = React.forwardRef( 41 | ({ className, variant, size, ...props }, ref) => { 42 | return ( 43 |