Upload demo files
#1
by Xenova HF Staff - opened
- .gitattributes +2 -0
- README.md +7 -2
- dist/CohereText-Regular.woff2 +0 -0
- dist/assets/index-C1v3--OK.js +0 -0
- dist/assets/index-D96roh91.css +2 -0
- dist/cohere.svg +13 -0
- dist/favicon.svg +1 -0
- dist/index.html +14 -0
- dist/video.mp4 +3 -0
- eslint.config.js +23 -0
- index.html +11 -17
- package.json +33 -0
- public/CohereText-Regular.woff2 +0 -0
- public/cohere.svg +13 -0
- public/favicon.svg +1 -0
- public/video.mp4 +3 -0
- src/App.tsx +635 -0
- src/Confetti.tsx +563 -0
- src/TranscriberContext.tsx +104 -0
- src/icons.tsx +152 -0
- src/index.css +135 -0
- src/main.tsx +13 -0
- src/transcriberContext.ts +24 -0
- src/utils.ts +21 -0
- style.css +0 -28
- tsconfig.app.json +28 -0
- tsconfig.json +7 -0
- tsconfig.node.json +26 -0
- vite.config.ts +8 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
dist/video.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
public/video.mp4 filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
| 1 |
---
|
| 2 |
title: Cohere Transcribe WebGPU
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: pink
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
title: Cohere Transcribe WebGPU
|
| 3 |
+
emoji: ⚡️
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: pink
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
thumbnail: >-
|
| 9 |
+
https://cdn-uploads.huggingface.co/production/uploads/61b253b7ac5ecaae3d1efe0c/AtkG76Hte5HytS3407lcp.png
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: Run Cohere Transcribe locally in your browser on WebGPU.
|
| 12 |
+
app_file: dist/index.html
|
| 13 |
---
|
| 14 |
|
| 15 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
dist/CohereText-Regular.woff2
ADDED
|
Binary file (35.1 kB). View file
|
|
|
dist/assets/index-C1v3--OK.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/assets/index-D96roh91.css
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */
|
| 2 |
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-emerald-600:oklch(59.6% .145 163.225);--color-white:#fff;--spacing:.25rem;--container-xl:36rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--leading-relaxed:1.625;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.inset-3{inset:calc(var(--spacing) * 3)}.start{inset-inline-start:var(--spacing)}.right-0{right:calc(var(--spacing) * 0)}.bottom-8{bottom:calc(var(--spacing) * 8)}.bottom-12{bottom:calc(var(--spacing) * 12)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-40{z-index:40}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-auto{margin-left:auto}.flex{display:flex}.hidden{display:none}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-16{height:calc(var(--spacing) * 16)}.h-72{height:calc(var(--spacing) * 72)}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[500px\]{max-height:500px}.min-h-\[280px\]{min-height:280px}.w-2{width:calc(var(--spacing) * 2)}.w-4\/6{width:66.6667%}.w-5\/6{width:83.3333%}.w-16{width:calc(var(--spacing) * 16)}.w-72{width:calc(var(--spacing) * 72)}.w-80{width:calc(var(--spacing) * 80)}.w-full{width:100%}.w-screen{width:100vw}.max-w-3xl{max-width:var(--container-3xl)}.max-w-xl{max-width:var(--container-xl)}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-\[var\(--cohere-border\)\]{border-color:var(--cohere-border)}.border-\[var\(--cohere-purple\)\]{border-color:var(--cohere-purple)}.border-red-200{border-color:var(--color-red-200)}.border-transparent{border-color:#0000}.bg-\[var\(--cohere-border\)\]{background-color:var(--cohere-border)}.bg-\[var\(--cohere-purple\)\]{background-color:var(--cohere-purple)}.bg-\[var\(--cohere-surface\)\]{background-color:var(--cohere-surface)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-500{background-color:var(--color-red-500)}.bg-white{background-color:var(--color-white)}.bg-white\/80{background-color:#fffc}@supports (color:color-mix(in lab, red, red)){.bg-white\/80{background-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.object-cover{object-fit:cover}.p-8{padding:calc(var(--spacing) * 8)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-8{padding-inline:calc(var(--spacing) * 8)}.px-16{padding-inline:calc(var(--spacing) * 16)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[var\(--cohere-purple\)\]{color:var(--cohere-purple)}.text-\[var\(--cohere-text\)\]{color:var(--cohere-text)}.text-\[var\(--cohere-text-muted\)\],.text-\[var\(--cohere-text-muted\)\]\/50{color:var(--cohere-text-muted)}@supports (color:color-mix(in lab, red, red)){.text-\[var\(--cohere-text-muted\)\]\/50{color:color-mix(in oklab, var(--cohere-text-muted) 50%, transparent)}}.text-emerald-600{color:var(--color-emerald-600)}.text-red-500{color:var(--color-red-500)}.text-white{color:var(--color-white)}.text-white\/60{color:#fff9}@supports (color:color-mix(in lab, red, red)){.text-white\/60{color:color-mix(in oklab, var(--color-white) 60%, transparent)}}.italic{font-style:italic}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}@media (hover:hover){.group-hover\:text-\[var\(--cohere-purple\)\]:is(:where(.group):hover *){color:var(--cohere-purple)}.hover\:border-\[var\(--cohere-purple\)\]:hover{border-color:var(--cohere-purple)}.hover\:bg-\[var\(--cohere-purple\)\]:hover{background-color:var(--cohere-purple)}.hover\:bg-red-100:hover{background-color:var(--color-red-100)}.hover\:text-\[var\(--cohere-purple\)\]:hover{color:var(--cohere-purple)}.hover\:text-white:hover{color:var(--color-white)}.hover\:shadow-\[0_0_60px_-15px_var\(--cohere-purple\)\]:hover{--tw-shadow:0 0 60px -15px var(--tw-shadow-color,var(--cohere-purple));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}@media (width>=40rem){.sm\:flex-row{flex-direction:row}}}@font-face{font-family:CohereText;src:url(/CohereText-Regular.woff2)format("woff2");font-weight:400;font-style:normal;font-display:swap}:root{--cohere-purple:#863bff;--cohere-deep-purple:#7e14ff;--cohere-cyan:#47bfff;--cohere-lavender:#ede6ff;--cohere-green:#355146;--cohere-bg:#fff;--cohere-surface:#f5f5f7;--cohere-surface-light:#eeeef0;--cohere-border:#e0e0e4;--cohere-text:#1a1a2e;--cohere-text-muted:#6b6b80}body{background:var(--cohere-bg);color:var(--cohere-text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin:0;font-family:CohereText,system-ui,-apple-system,sans-serif;overflow:hidden}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:var(--cohere-surface)}::-webkit-scrollbar-thumb{background:#ccc;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#aaa}.screen{transition:opacity .6s,transform .6s;position:absolute;inset:0}.screen-enter{opacity:1;z-index:10;transform:scale(1)}.screen-exit{opacity:0;pointer-events:none;z-index:5;transform:scale(1.02)}.screen-hidden{opacity:0;pointer-events:none;z-index:0;transform:scale(.98)}@keyframes pulse-glow{0%,to{opacity:.4}50%{opacity:1}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}@keyframes fade-in-up{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.animate-pulse-glow{animation:2s ease-in-out infinite pulse-glow}.animate-spin-slow{animation:1.2s linear infinite spin}.animate-fade-in-up{animation:.8s forwards fade-in-up}.animate-shimmer{background:linear-gradient(90deg, var(--cohere-surface) 25%, var(--cohere-surface-light) 50%, var(--cohere-surface) 75%);background-size:200% 100%;animation:1.5s ease-in-out infinite shimmer}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@keyframes pulse{50%{opacity:.5}}
|
dist/cohere.svg
ADDED
|
|
dist/favicon.svg
ADDED
|
|
dist/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" href="/favicon.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Cohere Transcribe — WebGPU</title>
|
| 8 |
+
<script type="module" crossorigin src="/assets/index-C1v3--OK.js"></script>
|
| 9 |
+
<link rel="stylesheet" crossorigin href="/assets/index-D96roh91.css">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<div id="root"></div>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
dist/video.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:781a69c1f92cf54a5e17382faaa4e5e4015af2602b013a711fb345e18f17e970
|
| 3 |
+
size 8667549
|
eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from "@eslint/js";
|
| 2 |
+
import globals from "globals";
|
| 3 |
+
import reactHooks from "eslint-plugin-react-hooks";
|
| 4 |
+
import reactRefresh from "eslint-plugin-react-refresh";
|
| 5 |
+
import tseslint from "typescript-eslint";
|
| 6 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 7 |
+
|
| 8 |
+
export default defineConfig([
|
| 9 |
+
globalIgnores(["dist"]),
|
| 10 |
+
{
|
| 11 |
+
files: ["**/*.{ts,tsx}"],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs.flat.recommended,
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
ecmaVersion: 2020,
|
| 20 |
+
globals: globals.browser,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
]);
|
index.html
CHANGED
|
@@ -1,19 +1,13 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
<p>
|
| 14 |
-
Also don't forget to check the
|
| 15 |
-
<a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
|
| 16 |
-
</p>
|
| 17 |
-
</div>
|
| 18 |
-
</body>
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" href="/favicon.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Cohere Transcribe — WebGPU</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
</html>
|
package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "cohere-transcribe-webgpu",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@huggingface/transformers": "4.0.0-next.10",
|
| 14 |
+
"@tailwindcss/vite": "^4.2.2",
|
| 15 |
+
"react": "^19.2.4",
|
| 16 |
+
"react-dom": "^19.2.4",
|
| 17 |
+
"tailwindcss": "^4.2.2"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@eslint/js": "^9.39.4",
|
| 21 |
+
"@types/node": "^24.12.0",
|
| 22 |
+
"@types/react": "^19.2.14",
|
| 23 |
+
"@types/react-dom": "^19.2.3",
|
| 24 |
+
"@vitejs/plugin-react": "^6.0.1",
|
| 25 |
+
"eslint": "^9.39.4",
|
| 26 |
+
"eslint-plugin-react-hooks": "^7.0.1",
|
| 27 |
+
"eslint-plugin-react-refresh": "^0.5.2",
|
| 28 |
+
"globals": "^17.4.0",
|
| 29 |
+
"typescript": "~5.9.3",
|
| 30 |
+
"typescript-eslint": "^8.57.0",
|
| 31 |
+
"vite": "^8.0.1"
|
| 32 |
+
}
|
| 33 |
+
}
|
public/CohereText-Regular.woff2
ADDED
|
Binary file (35.1 kB). View file
|
|
|
public/cohere.svg
ADDED
|
|
public/favicon.svg
ADDED
|
|
public/video.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:781a69c1f92cf54a5e17382faaa4e5e4015af2602b013a711fb345e18f17e970
|
| 3 |
+
size 8667549
|
src/App.tsx
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useCallback, useEffect } from "react";
|
| 2 |
+
import { useTranscriber } from "./transcriberContext.ts";
|
| 3 |
+
import Confetti, { type ConfettiHandle } from "./Confetti.tsx";
|
| 4 |
+
import { langToFlag } from "./utils.ts";
|
| 5 |
+
import {
|
| 6 |
+
CohereLogo,
|
| 7 |
+
UploadIcon,
|
| 8 |
+
MicrophoneIcon,
|
| 9 |
+
CopyIcon,
|
| 10 |
+
DownloadIcon,
|
| 11 |
+
CheckIcon,
|
| 12 |
+
FileIcon,
|
| 13 |
+
MicSmallIcon,
|
| 14 |
+
} from "./icons.tsx";
|
| 15 |
+
|
| 16 |
+
type Screen = "landing" | "loading" | "transcription";
|
| 17 |
+
type TranscriptionMode = "idle" | "file" | "microphone";
|
| 18 |
+
|
| 19 |
+
// ---- Constants ----
|
| 20 |
+
|
| 21 |
+
const SCREEN_TRANSITION_MS = 600; // must match .screen CSS transition duration in index.css
|
| 22 |
+
const COPY_FEEDBACK_MS = 2000;
|
| 23 |
+
const POST_LOAD_DELAY_MS = 500;
|
| 24 |
+
const AUDIO_SAMPLE_RATE = 16000;
|
| 25 |
+
|
| 26 |
+
const LANGUAGES: { code: string; label: string; native: string }[] = [
|
| 27 |
+
{ code: "en", label: "English", native: "English" },
|
| 28 |
+
{ code: "fr", label: "French", native: "Français" },
|
| 29 |
+
{ code: "de", label: "German", native: "Deutsch" },
|
| 30 |
+
{ code: "es", label: "Spanish", native: "Español" },
|
| 31 |
+
{ code: "it", label: "Italian", native: "Italiano" },
|
| 32 |
+
{ code: "pt", label: "Portuguese", native: "Português" },
|
| 33 |
+
{ code: "nl", label: "Dutch", native: "Nederlands" },
|
| 34 |
+
{ code: "pl", label: "Polish", native: "Polski" },
|
| 35 |
+
{ code: "el", label: "Greek", native: "Ελληνικά" },
|
| 36 |
+
{ code: "ar", label: "Arabic", native: "العربية" },
|
| 37 |
+
{ code: "ja", label: "Japanese", native: "日本語" },
|
| 38 |
+
{ code: "zh", label: "Chinese", native: "中文" },
|
| 39 |
+
{ code: "vi", label: "Vietnamese", native: "Tiếng Việt" },
|
| 40 |
+
{ code: "ko", label: "Korean", native: "한국어" },
|
| 41 |
+
];
|
| 42 |
+
|
| 43 |
+
// ---- Formatting helpers ----
|
| 44 |
+
|
| 45 |
+
function formatDuration(seconds: number): string {
|
| 46 |
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
| 47 |
+
const mins = Math.floor(seconds / 60);
|
| 48 |
+
const secs = seconds % 60;
|
| 49 |
+
return secs > 0 ? `${mins}m ${secs.toFixed(0)}s` : `${mins}m`;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// ---- Audio helpers ----
|
| 53 |
+
|
| 54 |
+
async function decodeAudio(arrayBuffer: ArrayBuffer): Promise<Float32Array> {
|
| 55 |
+
const audioCtx = new AudioContext({ sampleRate: AUDIO_SAMPLE_RATE });
|
| 56 |
+
const decoded = await audioCtx.decodeAudioData(arrayBuffer);
|
| 57 |
+
const float32 = decoded.getChannelData(0);
|
| 58 |
+
await audioCtx.close();
|
| 59 |
+
return float32;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// ---- Main App ----
|
| 63 |
+
|
| 64 |
+
function App() {
|
| 65 |
+
const [screen, setScreen] = useState<Screen>("landing");
|
| 66 |
+
const [prevScreen, setPrevScreen] = useState<Screen | null>(null);
|
| 67 |
+
const [mode, setMode] = useState<TranscriptionMode>("idle");
|
| 68 |
+
const [language, setLanguage] = useState("en");
|
| 69 |
+
const [transcriptionText, setTranscriptionText] = useState("");
|
| 70 |
+
const [streamedText, setStreamedText] = useState("");
|
| 71 |
+
const [isTranscribing, setIsTranscribing] = useState(false);
|
| 72 |
+
const [audioFileName, setAudioFileName] = useState<string | null>(null);
|
| 73 |
+
const [isRecording, setIsRecording] = useState(false);
|
| 74 |
+
const [copied, setCopied] = useState(false);
|
| 75 |
+
const [isDragging, setIsDragging] = useState(false);
|
| 76 |
+
const [stats, setStats] = useState<{
|
| 77 |
+
audioDuration: number;
|
| 78 |
+
elapsed: number;
|
| 79 |
+
} | null>(null);
|
| 80 |
+
|
| 81 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 82 |
+
const videoRef = useRef<HTMLVideoElement>(null);
|
| 83 |
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
| 84 |
+
const audioChunksRef = useRef<Blob[]>([]);
|
| 85 |
+
const outputRef = useRef<HTMLDivElement>(null);
|
| 86 |
+
const streamedTextRef = useRef("");
|
| 87 |
+
const confettiRef = useRef<ConfettiHandle>(null);
|
| 88 |
+
|
| 89 |
+
const transcriber = useTranscriber();
|
| 90 |
+
const displayText = isTranscribing ? streamedText : transcriptionText;
|
| 91 |
+
|
| 92 |
+
// ---- Screen transitions ----
|
| 93 |
+
|
| 94 |
+
const transitionTo = useCallback(
|
| 95 |
+
(next: Screen) => {
|
| 96 |
+
setPrevScreen(screen);
|
| 97 |
+
setScreen(next);
|
| 98 |
+
setTimeout(() => setPrevScreen(null), SCREEN_TRANSITION_MS);
|
| 99 |
+
},
|
| 100 |
+
[screen],
|
| 101 |
+
);
|
| 102 |
+
|
| 103 |
+
const getScreenClass = useCallback(
|
| 104 |
+
(s: Screen) => {
|
| 105 |
+
if (s === screen) return "screen screen-enter";
|
| 106 |
+
if (s === prevScreen) return "screen screen-exit";
|
| 107 |
+
return "screen screen-hidden";
|
| 108 |
+
},
|
| 109 |
+
[screen, prevScreen],
|
| 110 |
+
);
|
| 111 |
+
|
| 112 |
+
// ---- Video autoplay fallback ----
|
| 113 |
+
|
| 114 |
+
useEffect(() => {
|
| 115 |
+
if (screen === "landing" && videoRef.current) {
|
| 116 |
+
videoRef.current.play().catch(() => {});
|
| 117 |
+
}
|
| 118 |
+
}, [screen]);
|
| 119 |
+
|
| 120 |
+
// ---- Model loading: start when entering loading screen ----
|
| 121 |
+
|
| 122 |
+
useEffect(() => {
|
| 123 |
+
if (screen !== "loading") return;
|
| 124 |
+
transcriber.load().then(() => {
|
| 125 |
+
setTimeout(() => transitionTo("transcription"), POST_LOAD_DELAY_MS);
|
| 126 |
+
});
|
| 127 |
+
}, [screen, transcriber, transitionTo]);
|
| 128 |
+
|
| 129 |
+
// ---- Auto-scroll output during streaming ----
|
| 130 |
+
|
| 131 |
+
useEffect(() => {
|
| 132 |
+
if (isTranscribing && outputRef.current) {
|
| 133 |
+
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
| 134 |
+
}
|
| 135 |
+
}, [streamedText, isTranscribing]);
|
| 136 |
+
|
| 137 |
+
// ---- Streaming callback ----
|
| 138 |
+
|
| 139 |
+
const onToken = useCallback((token: string) => {
|
| 140 |
+
streamedTextRef.current += token;
|
| 141 |
+
setStreamedText(streamedTextRef.current);
|
| 142 |
+
}, []);
|
| 143 |
+
|
| 144 |
+
// ---- Run transcription (shared by file + mic) ----
|
| 145 |
+
|
| 146 |
+
const runTranscription = useCallback(
|
| 147 |
+
async (audio: Float32Array) => {
|
| 148 |
+
setIsTranscribing(true);
|
| 149 |
+
setTranscriptionText("");
|
| 150 |
+
setStreamedText("");
|
| 151 |
+
setStats(null);
|
| 152 |
+
streamedTextRef.current = "";
|
| 153 |
+
|
| 154 |
+
const audioDuration = audio.length / AUDIO_SAMPLE_RATE;
|
| 155 |
+
const startTime = performance.now();
|
| 156 |
+
|
| 157 |
+
try {
|
| 158 |
+
const finalText = await transcriber.transcribe(
|
| 159 |
+
audio,
|
| 160 |
+
language,
|
| 161 |
+
onToken,
|
| 162 |
+
);
|
| 163 |
+
const elapsed = (performance.now() - startTime) / 1000;
|
| 164 |
+
setTranscriptionText(finalText);
|
| 165 |
+
setStats({ audioDuration, elapsed });
|
| 166 |
+
} catch (err) {
|
| 167 |
+
setTranscriptionText(
|
| 168 |
+
`Error: ${err instanceof Error ? err.message : "Transcription failed"}`,
|
| 169 |
+
);
|
| 170 |
+
} finally {
|
| 171 |
+
setIsTranscribing(false);
|
| 172 |
+
}
|
| 173 |
+
},
|
| 174 |
+
[transcriber, language, onToken],
|
| 175 |
+
);
|
| 176 |
+
|
| 177 |
+
// ---- File handling (shared by input + drag-and-drop) ----
|
| 178 |
+
|
| 179 |
+
const processFile = useCallback(
|
| 180 |
+
async (file: File) => {
|
| 181 |
+
setAudioFileName(file.name);
|
| 182 |
+
setMode("file");
|
| 183 |
+
const audioData = await decodeAudio(await file.arrayBuffer());
|
| 184 |
+
runTranscription(audioData);
|
| 185 |
+
},
|
| 186 |
+
[runTranscription],
|
| 187 |
+
);
|
| 188 |
+
|
| 189 |
+
const handleFileSelect = useCallback(
|
| 190 |
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
| 191 |
+
const file = e.target.files?.[0];
|
| 192 |
+
if (!file) return;
|
| 193 |
+
processFile(file);
|
| 194 |
+
},
|
| 195 |
+
[processFile],
|
| 196 |
+
);
|
| 197 |
+
|
| 198 |
+
// ---- Drag and drop ----
|
| 199 |
+
|
| 200 |
+
const dragCounter = useRef(0);
|
| 201 |
+
|
| 202 |
+
const handleDragEnter = useCallback(
|
| 203 |
+
(e: React.DragEvent) => {
|
| 204 |
+
e.preventDefault();
|
| 205 |
+
if (screen !== "transcription" || mode !== "idle") return;
|
| 206 |
+
dragCounter.current++;
|
| 207 |
+
if (dragCounter.current === 1) setIsDragging(true);
|
| 208 |
+
},
|
| 209 |
+
[screen, mode],
|
| 210 |
+
);
|
| 211 |
+
|
| 212 |
+
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
| 213 |
+
e.preventDefault();
|
| 214 |
+
dragCounter.current--;
|
| 215 |
+
if (dragCounter.current === 0) setIsDragging(false);
|
| 216 |
+
}, []);
|
| 217 |
+
|
| 218 |
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
| 219 |
+
e.preventDefault();
|
| 220 |
+
}, []);
|
| 221 |
+
|
| 222 |
+
const handleDrop = useCallback(
|
| 223 |
+
(e: React.DragEvent) => {
|
| 224 |
+
e.preventDefault();
|
| 225 |
+
dragCounter.current = 0;
|
| 226 |
+
setIsDragging(false);
|
| 227 |
+
if (screen !== "transcription" || mode !== "idle") return;
|
| 228 |
+
const file = e.dataTransfer.files?.[0];
|
| 229 |
+
if (!file) return;
|
| 230 |
+
processFile(file);
|
| 231 |
+
},
|
| 232 |
+
[screen, mode, processFile],
|
| 233 |
+
);
|
| 234 |
+
|
| 235 |
+
// ---- Microphone ----
|
| 236 |
+
|
| 237 |
+
const startRecording = useCallback(async () => {
|
| 238 |
+
setMode("microphone");
|
| 239 |
+
setIsRecording(true);
|
| 240 |
+
setTranscriptionText("");
|
| 241 |
+
setStreamedText("");
|
| 242 |
+
audioChunksRef.current = [];
|
| 243 |
+
|
| 244 |
+
try {
|
| 245 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 246 |
+
const recorder = new MediaRecorder(stream);
|
| 247 |
+
mediaRecorderRef.current = recorder;
|
| 248 |
+
|
| 249 |
+
recorder.ondataavailable = (e) => {
|
| 250 |
+
if (e.data.size > 0) {
|
| 251 |
+
audioChunksRef.current.push(e.data);
|
| 252 |
+
}
|
| 253 |
+
};
|
| 254 |
+
|
| 255 |
+
recorder.onstop = async () => {
|
| 256 |
+
stream.getTracks().forEach((t) => t.stop());
|
| 257 |
+
setIsRecording(false);
|
| 258 |
+
|
| 259 |
+
try {
|
| 260 |
+
const blob = new Blob(audioChunksRef.current, { type: "audio/webm" });
|
| 261 |
+
const float32 = await decodeAudio(await blob.arrayBuffer());
|
| 262 |
+
runTranscription(float32);
|
| 263 |
+
} catch (err) {
|
| 264 |
+
setTranscriptionText(
|
| 265 |
+
`Error: ${err instanceof Error ? err.message : "Transcription failed"}`,
|
| 266 |
+
);
|
| 267 |
+
}
|
| 268 |
+
};
|
| 269 |
+
|
| 270 |
+
recorder.start();
|
| 271 |
+
} catch (err) {
|
| 272 |
+
setIsRecording(false);
|
| 273 |
+
setMode("idle");
|
| 274 |
+
console.error("Microphone access denied:", err);
|
| 275 |
+
}
|
| 276 |
+
}, [runTranscription]);
|
| 277 |
+
|
| 278 |
+
const stopRecording = useCallback(() => {
|
| 279 |
+
mediaRecorderRef.current?.stop();
|
| 280 |
+
}, []);
|
| 281 |
+
|
| 282 |
+
// ---- Copy to clipboard ----
|
| 283 |
+
|
| 284 |
+
const copyToClipboard = useCallback(() => {
|
| 285 |
+
navigator.clipboard.writeText(transcriptionText).then(() => {
|
| 286 |
+
setCopied(true);
|
| 287 |
+
setTimeout(() => setCopied(false), COPY_FEEDBACK_MS);
|
| 288 |
+
});
|
| 289 |
+
}, [transcriptionText]);
|
| 290 |
+
|
| 291 |
+
// ---- Download as .txt ----
|
| 292 |
+
|
| 293 |
+
const downloadText = useCallback(() => {
|
| 294 |
+
const blob = new Blob([transcriptionText], { type: "text/plain" });
|
| 295 |
+
const url = URL.createObjectURL(blob);
|
| 296 |
+
const a = document.createElement("a");
|
| 297 |
+
a.href = url;
|
| 298 |
+
a.download = "transcription.txt";
|
| 299 |
+
a.click();
|
| 300 |
+
URL.revokeObjectURL(url);
|
| 301 |
+
}, [transcriptionText]);
|
| 302 |
+
|
| 303 |
+
// ---- Reset ----
|
| 304 |
+
|
| 305 |
+
const resetTranscription = useCallback(() => {
|
| 306 |
+
setMode("idle");
|
| 307 |
+
setTranscriptionText("");
|
| 308 |
+
setStreamedText("");
|
| 309 |
+
streamedTextRef.current = "";
|
| 310 |
+
setIsTranscribing(false);
|
| 311 |
+
setAudioFileName(null);
|
| 312 |
+
setIsRecording(false);
|
| 313 |
+
setCopied(false);
|
| 314 |
+
setStats(null);
|
| 315 |
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
| 316 |
+
}, []);
|
| 317 |
+
|
| 318 |
+
// ---- Render ----
|
| 319 |
+
|
| 320 |
+
const isDone = !isTranscribing && !isRecording && !!transcriptionText;
|
| 321 |
+
|
| 322 |
+
return (
|
| 323 |
+
<div
|
| 324 |
+
className="relative w-screen h-screen overflow-hidden bg-white"
|
| 325 |
+
onDragEnter={handleDragEnter}
|
| 326 |
+
onDragLeave={handleDragLeave}
|
| 327 |
+
onDragOver={handleDragOver}
|
| 328 |
+
onDrop={handleDrop}
|
| 329 |
+
>
|
| 330 |
+
{/* ==================== Screen 1: Landing ==================== */}
|
| 331 |
+
<div
|
| 332 |
+
className={`${getScreenClass("landing")} cursor-pointer`}
|
| 333 |
+
onClick={() => screen === "landing" && transitionTo("loading")}
|
| 334 |
+
>
|
| 335 |
+
{/* Video background */}
|
| 336 |
+
<video
|
| 337 |
+
ref={videoRef}
|
| 338 |
+
className="absolute inset-0 w-full h-full object-cover"
|
| 339 |
+
src="/video.mp4"
|
| 340 |
+
autoPlay
|
| 341 |
+
loop
|
| 342 |
+
muted
|
| 343 |
+
playsInline
|
| 344 |
+
/>
|
| 345 |
+
|
| 346 |
+
{/* Click hint */}
|
| 347 |
+
<div className="absolute bottom-12 left-0 right-0 text-center z-10">
|
| 348 |
+
<p className="text-xl text-white animate-pulse-glow">
|
| 349 |
+
Click anywhere to begin
|
| 350 |
+
</p>
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
|
| 354 |
+
{/* ==================== Screen 2: Loading ==================== */}
|
| 355 |
+
<div
|
| 356 |
+
className={`${getScreenClass("loading")} flex flex-col items-center justify-center bg-white`}
|
| 357 |
+
>
|
| 358 |
+
<div className="flex flex-col items-center gap-8 animate-fade-in-up">
|
| 359 |
+
{/* Spinner */}
|
| 360 |
+
<div className="relative w-16 h-16">
|
| 361 |
+
<div
|
| 362 |
+
className="absolute inset-0 rounded-full animate-spin-slow"
|
| 363 |
+
style={{
|
| 364 |
+
border: "2px solid var(--cohere-border)",
|
| 365 |
+
borderTopColor: "var(--cohere-purple)",
|
| 366 |
+
borderRightColor: "var(--cohere-cyan)",
|
| 367 |
+
}}
|
| 368 |
+
/>
|
| 369 |
+
<div className="absolute inset-3 flex items-center justify-center">
|
| 370 |
+
<CohereLogo size={24} />
|
| 371 |
+
</div>
|
| 372 |
+
</div>
|
| 373 |
+
|
| 374 |
+
{/* Status text */}
|
| 375 |
+
<div className="flex flex-col items-center gap-4">
|
| 376 |
+
<p className="text-xl text-[var(--cohere-text)]">
|
| 377 |
+
Loading model...
|
| 378 |
+
</p>
|
| 379 |
+
|
| 380 |
+
{/* Progress bar */}
|
| 381 |
+
<div className="w-80 h-1.5 bg-[var(--cohere-border)] rounded-full overflow-hidden">
|
| 382 |
+
<div
|
| 383 |
+
className="h-full rounded-full transition-all duration-300 ease-out"
|
| 384 |
+
style={{
|
| 385 |
+
width: `${transcriber.progress}%`,
|
| 386 |
+
background:
|
| 387 |
+
"linear-gradient(90deg, var(--cohere-deep-purple), var(--cohere-purple), var(--cohere-cyan))",
|
| 388 |
+
}}
|
| 389 |
+
/>
|
| 390 |
+
</div>
|
| 391 |
+
|
| 392 |
+
<p className="text-sm text-[var(--cohere-text-muted)]">
|
| 393 |
+
{transcriber.statusText}
|
| 394 |
+
</p>
|
| 395 |
+
</div>
|
| 396 |
+
</div>
|
| 397 |
+
|
| 398 |
+
{/* Footer */}
|
| 399 |
+
<p className="absolute bottom-8 text-xs text-[var(--cohere-text-muted)]">
|
| 400 |
+
Powered by Transformers.js
|
| 401 |
+
</p>
|
| 402 |
+
</div>
|
| 403 |
+
|
| 404 |
+
{/* ==================== Screen 3: Transcription ==================== */}
|
| 405 |
+
<div
|
| 406 |
+
className={`${getScreenClass("transcription")} flex flex-col bg-white`}
|
| 407 |
+
>
|
| 408 |
+
{/* Header */}
|
| 409 |
+
<header className="flex items-center px-8 py-5 border-b border-[var(--cohere-border)]">
|
| 410 |
+
<img src="/cohere.svg" alt="Cohere" className="h-6" />
|
| 411 |
+
</header>
|
| 412 |
+
|
| 413 |
+
{/* Main content */}
|
| 414 |
+
<div className="flex-1 flex items-center justify-center px-8 py-10">
|
| 415 |
+
{mode === "idle" ? (
|
| 416 |
+
/* ---- Mode Selection + Language ---- */
|
| 417 |
+
<div className="flex flex-col items-center gap-10 animate-fade-in-up">
|
| 418 |
+
{/* Upload / Record cards */}
|
| 419 |
+
<div className="flex flex-col sm:flex-row gap-8">
|
| 420 |
+
{/* Upload File Card */}
|
| 421 |
+
<button
|
| 422 |
+
onClick={() => fileInputRef.current?.click()}
|
| 423 |
+
className="group w-72 h-72 rounded-2xl border border-[var(--cohere-border)] bg-[var(--cohere-surface)] hover:border-[var(--cohere-purple)] transition-all duration-300 cursor-pointer flex flex-col items-center justify-center gap-5 hover:shadow-[0_0_60px_-15px_var(--cohere-purple)]"
|
| 424 |
+
>
|
| 425 |
+
<div className="text-[var(--cohere-text-muted)] group-hover:text-[var(--cohere-purple)] transition-colors duration-300">
|
| 426 |
+
<UploadIcon />
|
| 427 |
+
</div>
|
| 428 |
+
<span className="text-xl text-[var(--cohere-text)]">
|
| 429 |
+
Choose File
|
| 430 |
+
</span>
|
| 431 |
+
<span className="text-base text-[var(--cohere-text-muted)]">
|
| 432 |
+
Select audio/video file
|
| 433 |
+
</span>
|
| 434 |
+
</button>
|
| 435 |
+
|
| 436 |
+
{/* Record Audio Card */}
|
| 437 |
+
<button
|
| 438 |
+
onClick={startRecording}
|
| 439 |
+
className="group w-72 h-72 rounded-2xl border border-[var(--cohere-border)] bg-[var(--cohere-surface)] hover:border-[var(--cohere-purple)] transition-all duration-300 cursor-pointer flex flex-col items-center justify-center gap-5 hover:shadow-[0_0_60px_-15px_var(--cohere-purple)]"
|
| 440 |
+
>
|
| 441 |
+
<div className="text-[var(--cohere-text-muted)] group-hover:text-[var(--cohere-purple)] transition-colors duration-300">
|
| 442 |
+
<MicrophoneIcon />
|
| 443 |
+
</div>
|
| 444 |
+
<span className="text-xl text-[var(--cohere-text)]">
|
| 445 |
+
Record Audio
|
| 446 |
+
</span>
|
| 447 |
+
<span className="text-base text-[var(--cohere-text-muted)]">
|
| 448 |
+
Use your microphone
|
| 449 |
+
</span>
|
| 450 |
+
</button>
|
| 451 |
+
</div>
|
| 452 |
+
|
| 453 |
+
{/* Language selector */}
|
| 454 |
+
<div className="flex flex-col items-center gap-3">
|
| 455 |
+
<span className="text-sm text-[var(--cohere-text-muted)]">
|
| 456 |
+
Language
|
| 457 |
+
</span>
|
| 458 |
+
<div className="flex flex-wrap justify-center gap-2 max-w-xl">
|
| 459 |
+
{LANGUAGES.map((lang) => (
|
| 460 |
+
<button
|
| 461 |
+
key={lang.code}
|
| 462 |
+
onClick={(e) => {
|
| 463 |
+
setLanguage(lang.code);
|
| 464 |
+
const rect = e.currentTarget.getBoundingClientRect();
|
| 465 |
+
confettiRef.current?.burst(
|
| 466 |
+
rect.left + rect.width / 2,
|
| 467 |
+
rect.top + rect.height / 2,
|
| 468 |
+
langToFlag(lang.code),
|
| 469 |
+
);
|
| 470 |
+
}}
|
| 471 |
+
className={`px-4 py-2 rounded-full text-sm transition-colors duration-200 cursor-pointer border ${
|
| 472 |
+
language === lang.code
|
| 473 |
+
? "bg-[var(--cohere-purple)] text-white border-transparent"
|
| 474 |
+
: "bg-[var(--cohere-surface)] text-[var(--cohere-text-muted)] border-[var(--cohere-border)] hover:border-[var(--cohere-purple)] hover:text-[var(--cohere-purple)]"
|
| 475 |
+
}`}
|
| 476 |
+
>
|
| 477 |
+
{lang.label}
|
| 478 |
+
{lang.label !== lang.native && (
|
| 479 |
+
<span
|
| 480 |
+
className={`ml-1 ${
|
| 481 |
+
language === lang.code
|
| 482 |
+
? "text-white/60"
|
| 483 |
+
: "text-[var(--cohere-text-muted)]/50"
|
| 484 |
+
}`}
|
| 485 |
+
>
|
| 486 |
+
/ {lang.native}
|
| 487 |
+
</span>
|
| 488 |
+
)}
|
| 489 |
+
</button>
|
| 490 |
+
))}
|
| 491 |
+
</div>
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
) : (
|
| 495 |
+
/* ---- Transcription Area ---- */
|
| 496 |
+
<div className="w-full max-w-3xl flex flex-col gap-6 animate-fade-in-up">
|
| 497 |
+
{/* Source indicator + status */}
|
| 498 |
+
<div className="flex items-center gap-3">
|
| 499 |
+
<div className="text-[var(--cohere-purple)]">
|
| 500 |
+
{mode === "file" ? <FileIcon /> : <MicSmallIcon />}
|
| 501 |
+
</div>
|
| 502 |
+
<span className="text-[var(--cohere-text)] text-base">
|
| 503 |
+
{mode === "file" ? audioFileName : "Microphone recording"}
|
| 504 |
+
</span>
|
| 505 |
+
|
| 506 |
+
{/* Recording controls */}
|
| 507 |
+
{isRecording && (
|
| 508 |
+
<div className="flex items-center gap-3 ml-auto">
|
| 509 |
+
<span className="flex items-center gap-2 text-sm text-red-500">
|
| 510 |
+
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
|
| 511 |
+
Recording...
|
| 512 |
+
</span>
|
| 513 |
+
<button
|
| 514 |
+
onClick={stopRecording}
|
| 515 |
+
className="px-4 py-1.5 text-sm bg-red-50 text-red-500 border border-red-200 rounded-lg hover:bg-red-100 transition-colors cursor-pointer"
|
| 516 |
+
>
|
| 517 |
+
Stop
|
| 518 |
+
</button>
|
| 519 |
+
</div>
|
| 520 |
+
)}
|
| 521 |
+
|
| 522 |
+
{/* Status badge */}
|
| 523 |
+
{isTranscribing && !isRecording && (
|
| 524 |
+
<span className="ml-auto flex items-center gap-2 text-sm text-[var(--cohere-text-muted)]">
|
| 525 |
+
<svg
|
| 526 |
+
className="animate-spin-slow"
|
| 527 |
+
width="14"
|
| 528 |
+
height="14"
|
| 529 |
+
viewBox="0 0 24 24"
|
| 530 |
+
fill="none"
|
| 531 |
+
stroke="currentColor"
|
| 532 |
+
strokeWidth="2"
|
| 533 |
+
>
|
| 534 |
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
| 535 |
+
</svg>
|
| 536 |
+
Transcribing...
|
| 537 |
+
</span>
|
| 538 |
+
)}
|
| 539 |
+
{isDone && (
|
| 540 |
+
<span className="ml-auto flex items-center gap-2 text-sm text-emerald-600">
|
| 541 |
+
<CheckIcon />
|
| 542 |
+
{stats
|
| 543 |
+
? `Transcribed ${formatDuration(stats.audioDuration)} of audio in ${formatDuration(stats.elapsed)}`
|
| 544 |
+
: "Complete"}
|
| 545 |
+
</span>
|
| 546 |
+
)}
|
| 547 |
+
</div>
|
| 548 |
+
|
| 549 |
+
{/* Transcription output */}
|
| 550 |
+
<div
|
| 551 |
+
ref={outputRef}
|
| 552 |
+
className="bg-[var(--cohere-surface)] rounded-xl p-8 min-h-[280px] max-h-[500px] overflow-y-auto border border-[var(--cohere-border)]"
|
| 553 |
+
>
|
| 554 |
+
{displayText ? (
|
| 555 |
+
<p className="text-xl leading-relaxed text-[var(--cohere-text)] whitespace-pre-wrap">
|
| 556 |
+
{displayText.trim()}
|
| 557 |
+
</p>
|
| 558 |
+
) : isRecording ? (
|
| 559 |
+
<p className="text-[var(--cohere-text-muted)] italic">
|
| 560 |
+
Listening... Press stop when you're done speaking.
|
| 561 |
+
</p>
|
| 562 |
+
) : isTranscribing ? (
|
| 563 |
+
<div className="space-y-3">
|
| 564 |
+
<div className="h-4 w-full rounded animate-shimmer" />
|
| 565 |
+
<div className="h-4 w-5/6 rounded animate-shimmer" />
|
| 566 |
+
<div className="h-4 w-4/6 rounded animate-shimmer" />
|
| 567 |
+
</div>
|
| 568 |
+
) : null}
|
| 569 |
+
</div>
|
| 570 |
+
|
| 571 |
+
{/* Actions */}
|
| 572 |
+
<div className="flex items-center justify-center gap-4">
|
| 573 |
+
{isDone && (
|
| 574 |
+
<>
|
| 575 |
+
<button
|
| 576 |
+
onClick={copyToClipboard}
|
| 577 |
+
className="flex items-center gap-2 px-5 py-2.5 text-base border border-[var(--cohere-border)] text-[var(--cohere-text-muted)] rounded-lg hover:border-[var(--cohere-purple)] hover:text-[var(--cohere-purple)] transition-all duration-200 cursor-pointer"
|
| 578 |
+
>
|
| 579 |
+
{copied ? <CheckIcon /> : <CopyIcon />}
|
| 580 |
+
{copied ? "Copied" : "Copy"}
|
| 581 |
+
</button>
|
| 582 |
+
<button
|
| 583 |
+
onClick={downloadText}
|
| 584 |
+
className="flex items-center gap-2 px-5 py-2.5 text-base border border-[var(--cohere-border)] text-[var(--cohere-text-muted)] rounded-lg hover:border-[var(--cohere-purple)] hover:text-[var(--cohere-purple)] transition-all duration-200 cursor-pointer"
|
| 585 |
+
>
|
| 586 |
+
<DownloadIcon />
|
| 587 |
+
Download
|
| 588 |
+
</button>
|
| 589 |
+
<button
|
| 590 |
+
onClick={resetTranscription}
|
| 591 |
+
className="flex items-center gap-2 px-5 py-2.5 text-base border border-[var(--cohere-purple)] text-[var(--cohere-purple)] rounded-lg hover:bg-[var(--cohere-purple)] hover:text-white transition-all duration-200 cursor-pointer"
|
| 592 |
+
>
|
| 593 |
+
New Transcription
|
| 594 |
+
</button>
|
| 595 |
+
</>
|
| 596 |
+
)}
|
| 597 |
+
</div>
|
| 598 |
+
</div>
|
| 599 |
+
)}
|
| 600 |
+
</div>
|
| 601 |
+
|
| 602 |
+
{/* Footer */}
|
| 603 |
+
<p className="pb-4 text-center text-xs text-[var(--cohere-text-muted)]">
|
| 604 |
+
Runs 100% locally in your browser with WebGPU
|
| 605 |
+
</p>
|
| 606 |
+
</div>
|
| 607 |
+
|
| 608 |
+
{/* Drag overlay */}
|
| 609 |
+
{isDragging && (
|
| 610 |
+
<div className="fixed inset-0 z-40 flex items-center justify-center bg-white/80 backdrop-blur-sm pointer-events-none">
|
| 611 |
+
<div className="flex flex-col items-center gap-4 rounded-2xl border-2 border-dashed border-[var(--cohere-purple)] bg-[var(--cohere-surface)] px-16 py-12">
|
| 612 |
+
<UploadIcon />
|
| 613 |
+
<p className="text-lg text-[var(--cohere-text)]">
|
| 614 |
+
Drop audio/video file here
|
| 615 |
+
</p>
|
| 616 |
+
</div>
|
| 617 |
+
</div>
|
| 618 |
+
)}
|
| 619 |
+
|
| 620 |
+
{/* Confetti overlay */}
|
| 621 |
+
<Confetti ref={confettiRef} />
|
| 622 |
+
|
| 623 |
+
{/* Hidden file input */}
|
| 624 |
+
<input
|
| 625 |
+
ref={fileInputRef}
|
| 626 |
+
type="file"
|
| 627 |
+
accept="audio/*,video/*"
|
| 628 |
+
className="hidden"
|
| 629 |
+
onChange={handleFileSelect}
|
| 630 |
+
/>
|
| 631 |
+
</div>
|
| 632 |
+
);
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
export default App;
|
src/Confetti.tsx
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
useRef,
|
| 3 |
+
useEffect,
|
| 4 |
+
useCallback,
|
| 5 |
+
useImperativeHandle,
|
| 6 |
+
forwardRef,
|
| 7 |
+
} from "react";
|
| 8 |
+
|
| 9 |
+
// ---- Types ----
|
| 10 |
+
|
| 11 |
+
interface Particle {
|
| 12 |
+
x: number;
|
| 13 |
+
y: number;
|
| 14 |
+
vx: number;
|
| 15 |
+
vy: number;
|
| 16 |
+
angle: number;
|
| 17 |
+
spin: number;
|
| 18 |
+
tiltPhase: number;
|
| 19 |
+
tiltSpeed: number;
|
| 20 |
+
wobblePhase: number;
|
| 21 |
+
wobbleSpeed: number;
|
| 22 |
+
wobbleAmp: number;
|
| 23 |
+
width: number;
|
| 24 |
+
height: number;
|
| 25 |
+
age: number;
|
| 26 |
+
life: number;
|
| 27 |
+
fadeIn: number;
|
| 28 |
+
fadeOutStart: number;
|
| 29 |
+
flagCode: string;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
interface Ribbon {
|
| 33 |
+
x: number;
|
| 34 |
+
y: number;
|
| 35 |
+
vx: number;
|
| 36 |
+
vy: number;
|
| 37 |
+
angle: number;
|
| 38 |
+
spin: number;
|
| 39 |
+
swayPhase: number;
|
| 40 |
+
swaySpeed: number;
|
| 41 |
+
swayAmp: number;
|
| 42 |
+
width: number;
|
| 43 |
+
length: number;
|
| 44 |
+
age: number;
|
| 45 |
+
life: number;
|
| 46 |
+
flagCode: string;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export interface ConfettiHandle {
|
| 50 |
+
burst: (x: number, y: number, flagCode: string) => void;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// ---- Constants ----
|
| 54 |
+
|
| 55 |
+
const TAU = Math.PI * 2;
|
| 56 |
+
const GRAVITY = 1080;
|
| 57 |
+
|
| 58 |
+
// ---- Helpers ----
|
| 59 |
+
|
| 60 |
+
function rand(min: number, max: number) {
|
| 61 |
+
return Math.random() * (max - min) + min;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function clamp(v: number, min: number, max: number) {
|
| 65 |
+
return Math.max(min, Math.min(max, v));
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function lerp(a: number, b: number, t: number) {
|
| 69 |
+
return a + (b - a) * t;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
function roundRect(
|
| 73 |
+
ctx: CanvasRenderingContext2D,
|
| 74 |
+
x: number,
|
| 75 |
+
y: number,
|
| 76 |
+
w: number,
|
| 77 |
+
h: number,
|
| 78 |
+
r: number,
|
| 79 |
+
) {
|
| 80 |
+
const rr = Math.min(r, w / 2, h / 2);
|
| 81 |
+
ctx.beginPath();
|
| 82 |
+
ctx.moveTo(x + rr, y);
|
| 83 |
+
ctx.arcTo(x + w, y, x + w, y + h, rr);
|
| 84 |
+
ctx.arcTo(x + w, y + h, x, y + h, rr);
|
| 85 |
+
ctx.arcTo(x, y + h, x, y, rr);
|
| 86 |
+
ctx.arcTo(x, y, x + w, y, rr);
|
| 87 |
+
ctx.closePath();
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// ---- Flag rendering ----
|
| 91 |
+
|
| 92 |
+
const flagCache = new Map<string, HTMLCanvasElement>();
|
| 93 |
+
|
| 94 |
+
function createFlagFace(
|
| 95 |
+
code: string,
|
| 96 |
+
side: "front" | "back",
|
| 97 |
+
): HTMLCanvasElement {
|
| 98 |
+
const key = `${code}-${side}`;
|
| 99 |
+
const cached = flagCache.get(key);
|
| 100 |
+
if (cached) return cached;
|
| 101 |
+
|
| 102 |
+
const off = document.createElement("canvas");
|
| 103 |
+
off.width = 160;
|
| 104 |
+
off.height = 108;
|
| 105 |
+
const g = off.getContext("2d")!;
|
| 106 |
+
const shade = side === "back";
|
| 107 |
+
|
| 108 |
+
const white = shade ? "#d6d9e3" : "#ffffff";
|
| 109 |
+
const red = shade ? "#aa4046" : "#c83b43";
|
| 110 |
+
const blue = shade ? "#2e4a82" : "#3c63b8";
|
| 111 |
+
const black = shade ? "#2c2d31" : "#111111";
|
| 112 |
+
const gold = shade ? "#b79a4c" : "#f0ca58";
|
| 113 |
+
const green = shade ? "#2f8456" : "#2fa86a";
|
| 114 |
+
const skyBlue = shade ? "#4a8ab0" : "#5bb5e8";
|
| 115 |
+
const darkRed = shade ? "#8a2020" : "#be1e2d";
|
| 116 |
+
const darkGreen = shade ? "#1a5e30" : "#1f8247";
|
| 117 |
+
|
| 118 |
+
const W = off.width;
|
| 119 |
+
const H = off.height;
|
| 120 |
+
|
| 121 |
+
g.clearRect(0, 0, W, H);
|
| 122 |
+
roundRect(g, 0, 0, W, H, 10);
|
| 123 |
+
g.clip();
|
| 124 |
+
|
| 125 |
+
switch (code) {
|
| 126 |
+
case "fr": {
|
| 127 |
+
g.fillStyle = blue;
|
| 128 |
+
g.fillRect(0, 0, W / 3, H);
|
| 129 |
+
g.fillStyle = white;
|
| 130 |
+
g.fillRect(W / 3, 0, W / 3, H);
|
| 131 |
+
g.fillStyle = red;
|
| 132 |
+
g.fillRect((W / 3) * 2, 0, W / 3, H);
|
| 133 |
+
break;
|
| 134 |
+
}
|
| 135 |
+
case "de": {
|
| 136 |
+
g.fillStyle = black;
|
| 137 |
+
g.fillRect(0, 0, W, H / 3);
|
| 138 |
+
g.fillStyle = red;
|
| 139 |
+
g.fillRect(0, H / 3, W, H / 3);
|
| 140 |
+
g.fillStyle = gold;
|
| 141 |
+
g.fillRect(0, (H / 3) * 2, W, H / 3);
|
| 142 |
+
break;
|
| 143 |
+
}
|
| 144 |
+
case "jp": {
|
| 145 |
+
g.fillStyle = white;
|
| 146 |
+
g.fillRect(0, 0, W, H);
|
| 147 |
+
g.fillStyle = red;
|
| 148 |
+
g.beginPath();
|
| 149 |
+
g.arc(W / 2, H / 2, H * 0.26, 0, TAU);
|
| 150 |
+
g.fill();
|
| 151 |
+
break;
|
| 152 |
+
}
|
| 153 |
+
case "it": {
|
| 154 |
+
g.fillStyle = green;
|
| 155 |
+
g.fillRect(0, 0, W / 3, H);
|
| 156 |
+
g.fillStyle = white;
|
| 157 |
+
g.fillRect(W / 3, 0, W / 3, H);
|
| 158 |
+
g.fillStyle = red;
|
| 159 |
+
g.fillRect((W / 3) * 2, 0, W / 3, H);
|
| 160 |
+
break;
|
| 161 |
+
}
|
| 162 |
+
case "es": {
|
| 163 |
+
g.fillStyle = red;
|
| 164 |
+
g.fillRect(0, 0, W, H / 4);
|
| 165 |
+
g.fillStyle = gold;
|
| 166 |
+
g.fillRect(0, H / 4, W, H / 2);
|
| 167 |
+
g.fillStyle = red;
|
| 168 |
+
g.fillRect(0, (H / 4) * 3, W, H / 4);
|
| 169 |
+
break;
|
| 170 |
+
}
|
| 171 |
+
case "pt": {
|
| 172 |
+
g.fillStyle = green;
|
| 173 |
+
g.fillRect(0, 0, W * 0.4, H);
|
| 174 |
+
g.fillStyle = red;
|
| 175 |
+
g.fillRect(W * 0.4, 0, W * 0.6, H);
|
| 176 |
+
// Simplified armillary sphere
|
| 177 |
+
g.fillStyle = gold;
|
| 178 |
+
g.beginPath();
|
| 179 |
+
g.arc(W * 0.4, H / 2, H * 0.22, 0, TAU);
|
| 180 |
+
g.fill();
|
| 181 |
+
break;
|
| 182 |
+
}
|
| 183 |
+
case "nl": {
|
| 184 |
+
g.fillStyle = darkRed;
|
| 185 |
+
g.fillRect(0, 0, W, H / 3);
|
| 186 |
+
g.fillStyle = white;
|
| 187 |
+
g.fillRect(0, H / 3, W, H / 3);
|
| 188 |
+
g.fillStyle = blue;
|
| 189 |
+
g.fillRect(0, (H / 3) * 2, W, H / 3);
|
| 190 |
+
break;
|
| 191 |
+
}
|
| 192 |
+
case "pl": {
|
| 193 |
+
g.fillStyle = white;
|
| 194 |
+
g.fillRect(0, 0, W, H / 2);
|
| 195 |
+
g.fillStyle = red;
|
| 196 |
+
g.fillRect(0, H / 2, W, H / 2);
|
| 197 |
+
break;
|
| 198 |
+
}
|
| 199 |
+
case "gr": {
|
| 200 |
+
// Greece: blue and white stripes with cross
|
| 201 |
+
const stripeH = H / 9;
|
| 202 |
+
for (let i = 0; i < 9; i++) {
|
| 203 |
+
g.fillStyle = i % 2 === 0 ? skyBlue : white;
|
| 204 |
+
g.fillRect(0, i * stripeH, W, stripeH + 0.5);
|
| 205 |
+
}
|
| 206 |
+
g.fillStyle = skyBlue;
|
| 207 |
+
g.fillRect(0, 0, W * 0.37, stripeH * 5);
|
| 208 |
+
g.fillStyle = white;
|
| 209 |
+
g.fillRect((W * 0.37) / 2 - stripeH * 0.5, 0, stripeH, stripeH * 5);
|
| 210 |
+
g.fillRect(0, stripeH * 2, W * 0.37, stripeH);
|
| 211 |
+
break;
|
| 212 |
+
}
|
| 213 |
+
case "sa": {
|
| 214 |
+
// Saudi Arabia (simplified): green with white
|
| 215 |
+
g.fillStyle = darkGreen;
|
| 216 |
+
g.fillRect(0, 0, W, H);
|
| 217 |
+
g.fillStyle = white;
|
| 218 |
+
g.font = `bold ${H * 0.18}px sans-serif`;
|
| 219 |
+
g.textAlign = "center";
|
| 220 |
+
g.textBaseline = "middle";
|
| 221 |
+
g.fillText("☪", W / 2, H / 2);
|
| 222 |
+
break;
|
| 223 |
+
}
|
| 224 |
+
case "cn": {
|
| 225 |
+
// China: red with yellow stars
|
| 226 |
+
g.fillStyle = red;
|
| 227 |
+
g.fillRect(0, 0, W, H);
|
| 228 |
+
g.fillStyle = gold;
|
| 229 |
+
g.beginPath();
|
| 230 |
+
g.arc(W * 0.22, H * 0.3, H * 0.14, 0, TAU);
|
| 231 |
+
g.fill();
|
| 232 |
+
// Small stars
|
| 233 |
+
for (const [sx, sy] of [
|
| 234 |
+
[0.38, 0.14],
|
| 235 |
+
[0.44, 0.24],
|
| 236 |
+
[0.44, 0.38],
|
| 237 |
+
[0.38, 0.48],
|
| 238 |
+
] as const) {
|
| 239 |
+
g.beginPath();
|
| 240 |
+
g.arc(W * sx, H * sy, H * 0.05, 0, TAU);
|
| 241 |
+
g.fill();
|
| 242 |
+
}
|
| 243 |
+
break;
|
| 244 |
+
}
|
| 245 |
+
case "vn": {
|
| 246 |
+
// Vietnam: red with yellow star
|
| 247 |
+
g.fillStyle = red;
|
| 248 |
+
g.fillRect(0, 0, W, H);
|
| 249 |
+
g.fillStyle = gold;
|
| 250 |
+
g.beginPath();
|
| 251 |
+
g.arc(W / 2, H / 2, H * 0.22, 0, TAU);
|
| 252 |
+
g.fill();
|
| 253 |
+
break;
|
| 254 |
+
}
|
| 255 |
+
case "kr": {
|
| 256 |
+
// South Korea (simplified): white with red/blue circle
|
| 257 |
+
g.fillStyle = white;
|
| 258 |
+
g.fillRect(0, 0, W, H);
|
| 259 |
+
const cx = W / 2,
|
| 260 |
+
cy = H / 2,
|
| 261 |
+
r = H * 0.24;
|
| 262 |
+
g.fillStyle = red;
|
| 263 |
+
g.beginPath();
|
| 264 |
+
g.arc(cx, cy, r, Math.PI, 0);
|
| 265 |
+
g.fill();
|
| 266 |
+
g.fillStyle = blue;
|
| 267 |
+
g.beginPath();
|
| 268 |
+
g.arc(cx, cy, r, 0, Math.PI);
|
| 269 |
+
g.fill();
|
| 270 |
+
break;
|
| 271 |
+
}
|
| 272 |
+
case "us":
|
| 273 |
+
default: {
|
| 274 |
+
const stripeH = H / 13;
|
| 275 |
+
for (let i = 0; i < 13; i++) {
|
| 276 |
+
g.fillStyle = i % 2 === 0 ? red : white;
|
| 277 |
+
g.fillRect(0, i * stripeH, W, stripeH + 0.5);
|
| 278 |
+
}
|
| 279 |
+
g.fillStyle = blue;
|
| 280 |
+
g.fillRect(0, 0, W * 0.46, stripeH * 7);
|
| 281 |
+
g.fillStyle = white;
|
| 282 |
+
for (let row = 0; row < 5; row++) {
|
| 283 |
+
for (let col = 0; col < 6; col++) {
|
| 284 |
+
const sx = 12 + col * 10 + (row % 2) * 4;
|
| 285 |
+
const sy = 10 + row * 10;
|
| 286 |
+
g.beginPath();
|
| 287 |
+
g.arc(sx, sy, 1.6, 0, TAU);
|
| 288 |
+
g.fill();
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
break;
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// Gloss / shade overlay
|
| 296 |
+
if (!shade) {
|
| 297 |
+
const gloss = g.createLinearGradient(0, 0, W, H);
|
| 298 |
+
gloss.addColorStop(0, "rgba(255,255,255,0.20)");
|
| 299 |
+
gloss.addColorStop(0.4, "rgba(255,255,255,0.02)");
|
| 300 |
+
gloss.addColorStop(1, "rgba(0,0,0,0.10)");
|
| 301 |
+
g.fillStyle = gloss;
|
| 302 |
+
g.fillRect(0, 0, W, H);
|
| 303 |
+
} else {
|
| 304 |
+
g.fillStyle = "rgba(0,0,0,0.22)";
|
| 305 |
+
g.fillRect(0, 0, W, H);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// Border
|
| 309 |
+
g.lineWidth = 2;
|
| 310 |
+
g.strokeStyle =
|
| 311 |
+
side === "front" ? "rgba(255,255,255,0.12)" : "rgba(255,255,255,0.08)";
|
| 312 |
+
roundRect(g, 1, 1, W - 2, H - 2, 10);
|
| 313 |
+
g.stroke();
|
| 314 |
+
|
| 315 |
+
flagCache.set(key, off);
|
| 316 |
+
return off;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
// ---- Particle factories ----
|
| 320 |
+
|
| 321 |
+
function makeParticle(x: number, y: number, flagCode: string): Particle {
|
| 322 |
+
const angle = rand(-Math.PI * 0.92, -Math.PI * 0.08);
|
| 323 |
+
const speed = rand(320, 860);
|
| 324 |
+
const size = rand(12, 24);
|
| 325 |
+
return {
|
| 326 |
+
x,
|
| 327 |
+
y,
|
| 328 |
+
vx: Math.cos(angle) * speed,
|
| 329 |
+
vy: Math.sin(angle) * speed,
|
| 330 |
+
angle: rand(0, TAU),
|
| 331 |
+
spin: rand(-10, 10),
|
| 332 |
+
tiltPhase: rand(0, TAU),
|
| 333 |
+
tiltSpeed: rand(8, 16),
|
| 334 |
+
wobblePhase: rand(0, TAU),
|
| 335 |
+
wobbleSpeed: rand(3.5, 8),
|
| 336 |
+
wobbleAmp: rand(4, 14),
|
| 337 |
+
width: size * rand(1.0, 1.35),
|
| 338 |
+
height: size * rand(0.55, 0.88),
|
| 339 |
+
age: 0,
|
| 340 |
+
life: rand(1.8, 2.85),
|
| 341 |
+
fadeIn: rand(0.04, 0.12),
|
| 342 |
+
fadeOutStart: rand(0.58, 0.78),
|
| 343 |
+
flagCode,
|
| 344 |
+
};
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
function makeRibbon(x: number, y: number, flagCode: string): Ribbon {
|
| 348 |
+
const angle = rand(-Math.PI * 0.92, -Math.PI * 0.08);
|
| 349 |
+
const speed = rand(280, 620);
|
| 350 |
+
return {
|
| 351 |
+
x,
|
| 352 |
+
y,
|
| 353 |
+
vx: Math.cos(angle) * speed,
|
| 354 |
+
vy: Math.sin(angle) * speed,
|
| 355 |
+
angle: rand(0, TAU),
|
| 356 |
+
spin: rand(-8, 8),
|
| 357 |
+
swayPhase: rand(0, TAU),
|
| 358 |
+
swaySpeed: rand(5, 10),
|
| 359 |
+
swayAmp: rand(5, 12),
|
| 360 |
+
width: rand(5, 7),
|
| 361 |
+
length: rand(22, 42),
|
| 362 |
+
age: 0,
|
| 363 |
+
life: rand(1.6, 2.3),
|
| 364 |
+
flagCode,
|
| 365 |
+
};
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
// ---- Alpha helper ----
|
| 369 |
+
|
| 370 |
+
function alphaFor(
|
| 371 |
+
age: number,
|
| 372 |
+
life: number,
|
| 373 |
+
fadeIn: number,
|
| 374 |
+
fadeOutStart: number,
|
| 375 |
+
) {
|
| 376 |
+
const t = age / life;
|
| 377 |
+
if (t < fadeIn) return t / fadeIn;
|
| 378 |
+
if (t < fadeOutStart) return 1;
|
| 379 |
+
return 1 - (t - fadeOutStart) / (1 - fadeOutStart);
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
// ---- Component ----
|
| 383 |
+
|
| 384 |
+
const Confetti = forwardRef<ConfettiHandle>(function Confetti(_props, ref) {
|
| 385 |
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
| 386 |
+
const particlesRef = useRef<Particle[]>([]);
|
| 387 |
+
const ribbonsRef = useRef<Ribbon[]>([]);
|
| 388 |
+
const rafRef = useRef<number | null>(null);
|
| 389 |
+
const lastTimeRef = useRef(0);
|
| 390 |
+
const sizeRef = useRef({ width: 0, height: 0, dpr: 1 });
|
| 391 |
+
|
| 392 |
+
// Resize handler
|
| 393 |
+
useEffect(() => {
|
| 394 |
+
function resize() {
|
| 395 |
+
const canvas = canvasRef.current;
|
| 396 |
+
if (!canvas) return;
|
| 397 |
+
const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
|
| 398 |
+
const w = window.innerWidth;
|
| 399 |
+
const h = window.innerHeight;
|
| 400 |
+
canvas.width = Math.round(w * dpr);
|
| 401 |
+
canvas.height = Math.round(h * dpr);
|
| 402 |
+
canvas.style.width = `${w}px`;
|
| 403 |
+
canvas.style.height = `${h}px`;
|
| 404 |
+
const ctx = canvas.getContext("2d");
|
| 405 |
+
ctx?.setTransform(dpr, 0, 0, dpr, 0, 0);
|
| 406 |
+
sizeRef.current = { width: w, height: h, dpr };
|
| 407 |
+
}
|
| 408 |
+
resize();
|
| 409 |
+
window.addEventListener("resize", resize, { passive: true });
|
| 410 |
+
return () => window.removeEventListener("resize", resize);
|
| 411 |
+
}, []);
|
| 412 |
+
|
| 413 |
+
// Animation loop
|
| 414 |
+
const tick = useCallback((now: number) => {
|
| 415 |
+
const canvas = canvasRef.current;
|
| 416 |
+
if (!canvas) return;
|
| 417 |
+
const ctx = canvas.getContext("2d");
|
| 418 |
+
if (!ctx) return;
|
| 419 |
+
|
| 420 |
+
const dt = Math.min(0.033, (now - lastTimeRef.current) / 1000 || 0.016);
|
| 421 |
+
lastTimeRef.current = now;
|
| 422 |
+
const { width, height } = sizeRef.current;
|
| 423 |
+
|
| 424 |
+
ctx.clearRect(0, 0, width, height);
|
| 425 |
+
|
| 426 |
+
const particles = particlesRef.current;
|
| 427 |
+
const ribbons = ribbonsRef.current;
|
| 428 |
+
|
| 429 |
+
// Update particles
|
| 430 |
+
particlesRef.current = particles.filter((p) => {
|
| 431 |
+
p.age += dt;
|
| 432 |
+
if (p.age >= p.life) return false;
|
| 433 |
+
p.vx *= Math.pow(0.992, dt * 60);
|
| 434 |
+
p.vy += GRAVITY * dt;
|
| 435 |
+
p.vx += Math.sin(now * 0.0016 + p.wobblePhase) * 14 * dt;
|
| 436 |
+
p.x += p.vx * dt;
|
| 437 |
+
p.y += p.vy * dt;
|
| 438 |
+
p.angle += p.spin * dt;
|
| 439 |
+
p.tiltPhase += p.tiltSpeed * dt;
|
| 440 |
+
p.wobblePhase += p.wobbleSpeed * dt;
|
| 441 |
+
return p.y < height + 120 && p.x > -160 && p.x < width + 160;
|
| 442 |
+
});
|
| 443 |
+
|
| 444 |
+
// Update ribbons
|
| 445 |
+
ribbonsRef.current = ribbons.filter((r) => {
|
| 446 |
+
r.age += dt;
|
| 447 |
+
if (r.age >= r.life) return false;
|
| 448 |
+
r.vx *= Math.pow(0.989, dt * 60);
|
| 449 |
+
r.vy += GRAVITY * 0.75 * dt;
|
| 450 |
+
r.vx += Math.cos(now * 0.0012 + r.swayPhase) * 14 * 1.1 * dt;
|
| 451 |
+
r.x += r.vx * dt;
|
| 452 |
+
r.y += r.vy * dt;
|
| 453 |
+
r.angle += r.spin * dt;
|
| 454 |
+
r.swayPhase += r.swaySpeed * dt;
|
| 455 |
+
return r.y < height + 120 && r.x > -160 && r.x < width + 160;
|
| 456 |
+
});
|
| 457 |
+
|
| 458 |
+
// Draw ribbons
|
| 459 |
+
for (const r of ribbonsRef.current) {
|
| 460 |
+
const front = createFlagFace(r.flagCode, "front");
|
| 461 |
+
const back = createFlagFace(r.flagCode, "back");
|
| 462 |
+
const progress = r.age / r.life;
|
| 463 |
+
const alpha = clamp(1 - Math.pow(progress, 2.2), 0, 1);
|
| 464 |
+
const flip = Math.cos(r.swayPhase);
|
| 465 |
+
const img = flip >= 0 ? front : back;
|
| 466 |
+
const stretch = lerp(0.32, 1, Math.abs(flip));
|
| 467 |
+
const swayX = Math.sin(r.swayPhase) * r.swayAmp;
|
| 468 |
+
|
| 469 |
+
ctx.save();
|
| 470 |
+
ctx.globalAlpha = alpha * 0.86;
|
| 471 |
+
ctx.translate(r.x + swayX, r.y);
|
| 472 |
+
ctx.rotate(r.angle);
|
| 473 |
+
ctx.scale(stretch, 1);
|
| 474 |
+
ctx.drawImage(img, -r.width / 2, -r.length / 2, r.width, r.length);
|
| 475 |
+
ctx.restore();
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
// Draw particles
|
| 479 |
+
for (const p of particlesRef.current) {
|
| 480 |
+
const front = createFlagFace(p.flagCode, "front");
|
| 481 |
+
const back = createFlagFace(p.flagCode, "back");
|
| 482 |
+
const alpha = clamp(
|
| 483 |
+
alphaFor(p.age, p.life, p.fadeIn, p.fadeOutStart),
|
| 484 |
+
0,
|
| 485 |
+
1,
|
| 486 |
+
);
|
| 487 |
+
const progress = p.age / p.life;
|
| 488 |
+
const flip = Math.cos(p.tiltPhase);
|
| 489 |
+
const scaleX = Math.sign(flip) * lerp(0.14, 1, Math.abs(flip));
|
| 490 |
+
const wobbleX = Math.sin(p.wobblePhase) * p.wobbleAmp;
|
| 491 |
+
const wobbleY = Math.cos(p.wobblePhase * 0.8) * (p.wobbleAmp * 0.22);
|
| 492 |
+
const img = flip >= 0 ? front : back;
|
| 493 |
+
|
| 494 |
+
ctx.save();
|
| 495 |
+
ctx.globalAlpha = alpha * clamp(1 - progress * 0.1, 0.72, 1);
|
| 496 |
+
ctx.translate(p.x + wobbleX, p.y + wobbleY);
|
| 497 |
+
ctx.rotate(p.angle);
|
| 498 |
+
ctx.scale(scaleX, 1);
|
| 499 |
+
ctx.drawImage(img, -p.width / 2, -p.height / 2, p.width, p.height);
|
| 500 |
+
ctx.restore();
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
if (particlesRef.current.length || ribbonsRef.current.length) {
|
| 504 |
+
rafRef.current = requestAnimationFrame(tick);
|
| 505 |
+
} else {
|
| 506 |
+
rafRef.current = null;
|
| 507 |
+
}
|
| 508 |
+
}, []);
|
| 509 |
+
|
| 510 |
+
// Cleanup on unmount
|
| 511 |
+
useEffect(() => {
|
| 512 |
+
return () => {
|
| 513 |
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
| 514 |
+
};
|
| 515 |
+
}, []);
|
| 516 |
+
|
| 517 |
+
// Expose burst method
|
| 518 |
+
const burst = useCallback(
|
| 519 |
+
(x: number, y: number, flagCode: string) => {
|
| 520 |
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
| 521 |
+
|
| 522 |
+
const { width, height } = sizeRef.current;
|
| 523 |
+
const areaFactor = clamp((width * height) / (1440 * 900), 0.72, 1.3);
|
| 524 |
+
const particleCount = Math.round(80 * areaFactor);
|
| 525 |
+
const ribbonCount = Math.round(14 * areaFactor);
|
| 526 |
+
|
| 527 |
+
for (let i = 0; i < particleCount; i++) {
|
| 528 |
+
particlesRef.current.push(
|
| 529 |
+
makeParticle(x + rand(-8, 8), y + rand(-8, 8), flagCode),
|
| 530 |
+
);
|
| 531 |
+
}
|
| 532 |
+
for (let i = 0; i < ribbonCount; i++) {
|
| 533 |
+
ribbonsRef.current.push(
|
| 534 |
+
makeRibbon(x + rand(-12, 12), y + rand(-6, 6), flagCode),
|
| 535 |
+
);
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
if (!rafRef.current) {
|
| 539 |
+
lastTimeRef.current = performance.now();
|
| 540 |
+
rafRef.current = requestAnimationFrame(tick);
|
| 541 |
+
}
|
| 542 |
+
},
|
| 543 |
+
[tick],
|
| 544 |
+
);
|
| 545 |
+
|
| 546 |
+
useImperativeHandle(ref, () => ({ burst }), [burst]);
|
| 547 |
+
|
| 548 |
+
return (
|
| 549 |
+
<canvas
|
| 550 |
+
ref={canvasRef}
|
| 551 |
+
style={{
|
| 552 |
+
position: "fixed",
|
| 553 |
+
inset: 0,
|
| 554 |
+
width: "100%",
|
| 555 |
+
height: "100%",
|
| 556 |
+
pointerEvents: "none",
|
| 557 |
+
zIndex: 50,
|
| 558 |
+
}}
|
| 559 |
+
/>
|
| 560 |
+
);
|
| 561 |
+
});
|
| 562 |
+
|
| 563 |
+
export default Confetti;
|
src/TranscriberContext.tsx
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef, useState, useCallback, type ReactNode } from "react";
|
| 2 |
+
import {
|
| 3 |
+
pipeline,
|
| 4 |
+
TextStreamer,
|
| 5 |
+
type AutomaticSpeechRecognitionPipeline,
|
| 6 |
+
type AutomaticSpeechRecognitionOutput,
|
| 7 |
+
} from "@huggingface/transformers";
|
| 8 |
+
import {
|
| 9 |
+
TranscriberContext,
|
| 10 |
+
type TranscriberState,
|
| 11 |
+
} from "./transcriberContext.ts";
|
| 12 |
+
|
| 13 |
+
const MODEL_ID = "onnx-community/cohere-transcribe-03-2026-ONNX";
|
| 14 |
+
|
| 15 |
+
export function TranscriberProvider({ children }: { children: ReactNode }) {
|
| 16 |
+
const [status, setStatus] = useState<TranscriberState["status"]>("idle");
|
| 17 |
+
const [error, setError] = useState<string | null>(null);
|
| 18 |
+
const [progress, setProgress] = useState(0);
|
| 19 |
+
const [statusText, setStatusText] = useState("Initializing...");
|
| 20 |
+
const pipelineRef = useRef<AutomaticSpeechRecognitionPipeline | null>(null);
|
| 21 |
+
const loadingRef = useRef<Promise<void> | null>(null);
|
| 22 |
+
|
| 23 |
+
const load = useCallback(async () => {
|
| 24 |
+
if (pipelineRef.current) return;
|
| 25 |
+
if (loadingRef.current) return loadingRef.current;
|
| 26 |
+
|
| 27 |
+
const loadPromise = (async () => {
|
| 28 |
+
setStatus("loading");
|
| 29 |
+
setProgress(0);
|
| 30 |
+
setStatusText("Downloading model...");
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
const transcriber = await pipeline(
|
| 34 |
+
"automatic-speech-recognition",
|
| 35 |
+
MODEL_ID,
|
| 36 |
+
{
|
| 37 |
+
dtype: "q4",
|
| 38 |
+
device: "webgpu",
|
| 39 |
+
progress_callback: (info: {
|
| 40 |
+
status: string;
|
| 41 |
+
progress?: number;
|
| 42 |
+
}) => {
|
| 43 |
+
if (info.status === "progress_total") {
|
| 44 |
+
const pct = Math.round(info.progress ?? 0);
|
| 45 |
+
setProgress(pct);
|
| 46 |
+
setStatusText(`Loading model... ${pct}%`);
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
},
|
| 50 |
+
);
|
| 51 |
+
pipelineRef.current = transcriber;
|
| 52 |
+
setProgress(100);
|
| 53 |
+
setStatusText("Ready");
|
| 54 |
+
setStatus("ready");
|
| 55 |
+
} catch (err) {
|
| 56 |
+
console.error("Failed to load transcription model:", err);
|
| 57 |
+
const message =
|
| 58 |
+
err instanceof Error ? err.message : "Failed to load model";
|
| 59 |
+
setStatus("error");
|
| 60 |
+
setError(message);
|
| 61 |
+
setStatusText(message);
|
| 62 |
+
}
|
| 63 |
+
})();
|
| 64 |
+
|
| 65 |
+
loadingRef.current = loadPromise;
|
| 66 |
+
return loadPromise;
|
| 67 |
+
}, []);
|
| 68 |
+
|
| 69 |
+
const transcribe = useCallback(
|
| 70 |
+
async (
|
| 71 |
+
audio: Float32Array,
|
| 72 |
+
language?: string,
|
| 73 |
+
onToken?: (token: string) => void,
|
| 74 |
+
) => {
|
| 75 |
+
if (!pipelineRef.current) {
|
| 76 |
+
throw new Error("Model not loaded");
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
const streamer = onToken
|
| 80 |
+
? new TextStreamer(pipelineRef.current.tokenizer, {
|
| 81 |
+
skip_prompt: true,
|
| 82 |
+
skip_special_tokens: true,
|
| 83 |
+
callback_function: onToken,
|
| 84 |
+
})
|
| 85 |
+
: undefined;
|
| 86 |
+
|
| 87 |
+
const result = (await pipelineRef.current(audio, {
|
| 88 |
+
max_new_tokens: 1024,
|
| 89 |
+
language,
|
| 90 |
+
streamer,
|
| 91 |
+
})) as AutomaticSpeechRecognitionOutput;
|
| 92 |
+
return result.text;
|
| 93 |
+
},
|
| 94 |
+
[],
|
| 95 |
+
);
|
| 96 |
+
|
| 97 |
+
return (
|
| 98 |
+
<TranscriberContext.Provider
|
| 99 |
+
value={{ status, error, progress, statusText, load, transcribe }}
|
| 100 |
+
>
|
| 101 |
+
{children}
|
| 102 |
+
</TranscriberContext.Provider>
|
| 103 |
+
);
|
| 104 |
+
}
|
src/icons.tsx
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function CohereLogo({ size = 48 }: { size?: number }) {
|
| 2 |
+
return (
|
| 3 |
+
<svg
|
| 4 |
+
viewBox="0 0 24 24"
|
| 5 |
+
width={size}
|
| 6 |
+
height={size}
|
| 7 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 8 |
+
>
|
| 9 |
+
<path
|
| 10 |
+
clipRule="evenodd"
|
| 11 |
+
d="M8.128 14.099c.592 0 1.77-.033 3.398-.703 1.897-.781 5.672-2.2 8.395-3.656 1.905-1.018 2.74-2.366 2.74-4.18A4.56 4.56 0 0018.1 1H7.549A6.55 6.55 0 001 7.55c0 3.617 2.745 6.549 7.128 6.549z"
|
| 12 |
+
fill="#39594D"
|
| 13 |
+
fillRule="evenodd"
|
| 14 |
+
></path>
|
| 15 |
+
<path
|
| 16 |
+
clipRule="evenodd"
|
| 17 |
+
d="M9.912 18.61a4.387 4.387 0 012.705-4.052l3.323-1.38c3.361-1.394 7.06 1.076 7.06 4.715a5.104 5.104 0 01-5.105 5.104l-3.597-.001a4.386 4.386 0 01-4.386-4.387z"
|
| 18 |
+
fill="#D18EE2"
|
| 19 |
+
fillRule="evenodd"
|
| 20 |
+
></path>
|
| 21 |
+
<path
|
| 22 |
+
d="M4.776 14.962A3.775 3.775 0 001 18.738v.489a3.776 3.776 0 007.551 0v-.49a3.775 3.775 0 00-3.775-3.775z"
|
| 23 |
+
fill="#FF7759"
|
| 24 |
+
></path>
|
| 25 |
+
</svg>
|
| 26 |
+
);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export function UploadIcon() {
|
| 30 |
+
return (
|
| 31 |
+
<svg
|
| 32 |
+
width="48"
|
| 33 |
+
height="48"
|
| 34 |
+
viewBox="0 0 24 24"
|
| 35 |
+
fill="none"
|
| 36 |
+
stroke="currentColor"
|
| 37 |
+
strokeWidth="1.5"
|
| 38 |
+
strokeLinecap="round"
|
| 39 |
+
strokeLinejoin="round"
|
| 40 |
+
>
|
| 41 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
| 42 |
+
<polyline points="17 8 12 3 7 8" />
|
| 43 |
+
<line x1="12" y1="3" x2="12" y2="15" />
|
| 44 |
+
</svg>
|
| 45 |
+
);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export function MicrophoneIcon() {
|
| 49 |
+
return (
|
| 50 |
+
<svg
|
| 51 |
+
width="48"
|
| 52 |
+
height="48"
|
| 53 |
+
viewBox="0 0 24 24"
|
| 54 |
+
fill="none"
|
| 55 |
+
stroke="currentColor"
|
| 56 |
+
strokeWidth="1.5"
|
| 57 |
+
strokeLinecap="round"
|
| 58 |
+
strokeLinejoin="round"
|
| 59 |
+
>
|
| 60 |
+
<rect x="9" y="2" width="6" height="11" rx="3" />
|
| 61 |
+
<path d="M5 10a7 7 0 0 0 14 0" />
|
| 62 |
+
<line x1="12" y1="17" x2="12" y2="21" />
|
| 63 |
+
<line x1="8" y1="21" x2="16" y2="21" />
|
| 64 |
+
</svg>
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
export function CopyIcon() {
|
| 69 |
+
return (
|
| 70 |
+
<svg
|
| 71 |
+
width="16"
|
| 72 |
+
height="16"
|
| 73 |
+
viewBox="0 0 24 24"
|
| 74 |
+
fill="none"
|
| 75 |
+
stroke="currentColor"
|
| 76 |
+
strokeWidth="1.5"
|
| 77 |
+
strokeLinecap="round"
|
| 78 |
+
strokeLinejoin="round"
|
| 79 |
+
>
|
| 80 |
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
| 81 |
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
| 82 |
+
</svg>
|
| 83 |
+
);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
export function DownloadIcon() {
|
| 87 |
+
return (
|
| 88 |
+
<svg
|
| 89 |
+
width="16"
|
| 90 |
+
height="16"
|
| 91 |
+
viewBox="0 0 24 24"
|
| 92 |
+
fill="none"
|
| 93 |
+
stroke="currentColor"
|
| 94 |
+
strokeWidth="1.5"
|
| 95 |
+
strokeLinecap="round"
|
| 96 |
+
strokeLinejoin="round"
|
| 97 |
+
>
|
| 98 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
| 99 |
+
<polyline points="7 10 12 15 17 10" />
|
| 100 |
+
<line x1="12" y1="15" x2="12" y2="3" />
|
| 101 |
+
</svg>
|
| 102 |
+
);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
export function CheckIcon() {
|
| 106 |
+
return (
|
| 107 |
+
<svg
|
| 108 |
+
width="16"
|
| 109 |
+
height="16"
|
| 110 |
+
viewBox="0 0 24 24"
|
| 111 |
+
fill="none"
|
| 112 |
+
stroke="currentColor"
|
| 113 |
+
strokeWidth="2"
|
| 114 |
+
strokeLinecap="round"
|
| 115 |
+
strokeLinejoin="round"
|
| 116 |
+
>
|
| 117 |
+
<polyline points="20 6 9 17 4 12" />
|
| 118 |
+
</svg>
|
| 119 |
+
);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
export function FileIcon() {
|
| 123 |
+
return (
|
| 124 |
+
<svg
|
| 125 |
+
width="20"
|
| 126 |
+
height="20"
|
| 127 |
+
viewBox="0 0 24 24"
|
| 128 |
+
fill="none"
|
| 129 |
+
stroke="currentColor"
|
| 130 |
+
strokeWidth="1.5"
|
| 131 |
+
>
|
| 132 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
| 133 |
+
<polyline points="14 2 14 8 20 8" />
|
| 134 |
+
</svg>
|
| 135 |
+
);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
export function MicSmallIcon() {
|
| 139 |
+
return (
|
| 140 |
+
<svg
|
| 141 |
+
width="20"
|
| 142 |
+
height="20"
|
| 143 |
+
viewBox="0 0 24 24"
|
| 144 |
+
fill="none"
|
| 145 |
+
stroke="currentColor"
|
| 146 |
+
strokeWidth="1.5"
|
| 147 |
+
>
|
| 148 |
+
<rect x="9" y="2" width="6" height="11" rx="3" />
|
| 149 |
+
<path d="M5 10a7 7 0 0 0 14 0" />
|
| 150 |
+
</svg>
|
| 151 |
+
);
|
| 152 |
+
}
|
src/index.css
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
/* ---- Font ---- */
|
| 4 |
+
@font-face {
|
| 5 |
+
font-family: "CohereText";
|
| 6 |
+
src: url("/CohereText-Regular.woff2") format("woff2");
|
| 7 |
+
font-weight: 400;
|
| 8 |
+
font-style: normal;
|
| 9 |
+
font-display: swap;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/* ---- CSS Custom Properties ---- */
|
| 13 |
+
:root {
|
| 14 |
+
--cohere-purple: #863bff;
|
| 15 |
+
--cohere-deep-purple: #7e14ff;
|
| 16 |
+
--cohere-cyan: #47bfff;
|
| 17 |
+
--cohere-lavender: #ede6ff;
|
| 18 |
+
--cohere-green: #355146;
|
| 19 |
+
--cohere-bg: #ffffff;
|
| 20 |
+
--cohere-surface: #f5f5f7;
|
| 21 |
+
--cohere-surface-light: #eeeef0;
|
| 22 |
+
--cohere-border: #e0e0e4;
|
| 23 |
+
--cohere-text: #1a1a2e;
|
| 24 |
+
--cohere-text-muted: #6b6b80;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* ---- Global ---- */
|
| 28 |
+
body {
|
| 29 |
+
background: var(--cohere-bg);
|
| 30 |
+
color: var(--cohere-text);
|
| 31 |
+
font-family:
|
| 32 |
+
"CohereText",
|
| 33 |
+
system-ui,
|
| 34 |
+
-apple-system,
|
| 35 |
+
sans-serif;
|
| 36 |
+
-webkit-font-smoothing: antialiased;
|
| 37 |
+
-moz-osx-font-smoothing: grayscale;
|
| 38 |
+
overflow: hidden;
|
| 39 |
+
margin: 0;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* ---- Scrollbar ---- */
|
| 43 |
+
::-webkit-scrollbar {
|
| 44 |
+
width: 6px;
|
| 45 |
+
}
|
| 46 |
+
::-webkit-scrollbar-track {
|
| 47 |
+
background: var(--cohere-surface);
|
| 48 |
+
}
|
| 49 |
+
::-webkit-scrollbar-thumb {
|
| 50 |
+
background: #ccc;
|
| 51 |
+
border-radius: 3px;
|
| 52 |
+
}
|
| 53 |
+
::-webkit-scrollbar-thumb:hover {
|
| 54 |
+
background: #aaa;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* ---- Screen Transitions ---- */
|
| 58 |
+
.screen {
|
| 59 |
+
position: absolute;
|
| 60 |
+
inset: 0;
|
| 61 |
+
transition:
|
| 62 |
+
opacity 0.6s ease,
|
| 63 |
+
transform 0.6s ease;
|
| 64 |
+
}
|
| 65 |
+
.screen-enter {
|
| 66 |
+
opacity: 1;
|
| 67 |
+
transform: scale(1);
|
| 68 |
+
z-index: 10;
|
| 69 |
+
}
|
| 70 |
+
.screen-exit {
|
| 71 |
+
opacity: 0;
|
| 72 |
+
transform: scale(1.02);
|
| 73 |
+
pointer-events: none;
|
| 74 |
+
z-index: 5;
|
| 75 |
+
}
|
| 76 |
+
.screen-hidden {
|
| 77 |
+
opacity: 0;
|
| 78 |
+
transform: scale(0.98);
|
| 79 |
+
pointer-events: none;
|
| 80 |
+
z-index: 0;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/* ---- Keyframes ---- */
|
| 84 |
+
@keyframes pulse-glow {
|
| 85 |
+
0%,
|
| 86 |
+
100% {
|
| 87 |
+
opacity: 0.4;
|
| 88 |
+
}
|
| 89 |
+
50% {
|
| 90 |
+
opacity: 1;
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
@keyframes spin {
|
| 94 |
+
to {
|
| 95 |
+
transform: rotate(360deg);
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
@keyframes shimmer {
|
| 99 |
+
0% {
|
| 100 |
+
background-position: -200% 0;
|
| 101 |
+
}
|
| 102 |
+
100% {
|
| 103 |
+
background-position: 200% 0;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
@keyframes fade-in-up {
|
| 107 |
+
from {
|
| 108 |
+
opacity: 0;
|
| 109 |
+
transform: translateY(20px);
|
| 110 |
+
}
|
| 111 |
+
to {
|
| 112 |
+
opacity: 1;
|
| 113 |
+
transform: translateY(0);
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.animate-pulse-glow {
|
| 118 |
+
animation: pulse-glow 2s ease-in-out infinite;
|
| 119 |
+
}
|
| 120 |
+
.animate-spin-slow {
|
| 121 |
+
animation: spin 1.2s linear infinite;
|
| 122 |
+
}
|
| 123 |
+
.animate-fade-in-up {
|
| 124 |
+
animation: fade-in-up 0.8s ease forwards;
|
| 125 |
+
}
|
| 126 |
+
.animate-shimmer {
|
| 127 |
+
background: linear-gradient(
|
| 128 |
+
90deg,
|
| 129 |
+
var(--cohere-surface) 25%,
|
| 130 |
+
var(--cohere-surface-light) 50%,
|
| 131 |
+
var(--cohere-surface) 75%
|
| 132 |
+
);
|
| 133 |
+
background-size: 200% 100%;
|
| 134 |
+
animation: shimmer 1.5s ease-in-out infinite;
|
| 135 |
+
}
|
src/main.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from "react";
|
| 2 |
+
import { createRoot } from "react-dom/client";
|
| 3 |
+
import "./index.css";
|
| 4 |
+
import App from "./App.tsx";
|
| 5 |
+
import { TranscriberProvider } from "./TranscriberContext.tsx";
|
| 6 |
+
|
| 7 |
+
createRoot(document.getElementById("root")!).render(
|
| 8 |
+
<StrictMode>
|
| 9 |
+
<TranscriberProvider>
|
| 10 |
+
<App />
|
| 11 |
+
</TranscriberProvider>
|
| 12 |
+
</StrictMode>,
|
| 13 |
+
);
|
src/transcriberContext.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createContext, useContext } from "react";
|
| 2 |
+
|
| 3 |
+
export interface TranscriberState {
|
| 4 |
+
status: "idle" | "loading" | "ready" | "error";
|
| 5 |
+
error: string | null;
|
| 6 |
+
progress: number;
|
| 7 |
+
statusText: string;
|
| 8 |
+
load: () => Promise<void>;
|
| 9 |
+
transcribe: (
|
| 10 |
+
audio: Float32Array,
|
| 11 |
+
language?: string,
|
| 12 |
+
onToken?: (token: string) => void,
|
| 13 |
+
) => Promise<string>;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export const TranscriberContext = createContext<TranscriberState | null>(null);
|
| 17 |
+
|
| 18 |
+
export function useTranscriber() {
|
| 19 |
+
const ctx = useContext(TranscriberContext);
|
| 20 |
+
if (!ctx) {
|
| 21 |
+
throw new Error("useTranscriber must be used within TranscriberProvider");
|
| 22 |
+
}
|
| 23 |
+
return ctx;
|
| 24 |
+
}
|
src/utils.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Map language codes to flag rendering codes
|
| 2 |
+
const LANG_TO_FLAG: Record<string, string> = {
|
| 3 |
+
en: "us",
|
| 4 |
+
fr: "fr",
|
| 5 |
+
de: "de",
|
| 6 |
+
es: "es",
|
| 7 |
+
it: "it",
|
| 8 |
+
pt: "pt",
|
| 9 |
+
nl: "nl",
|
| 10 |
+
pl: "pl",
|
| 11 |
+
el: "gr",
|
| 12 |
+
ar: "sa",
|
| 13 |
+
ja: "jp",
|
| 14 |
+
zh: "cn",
|
| 15 |
+
vi: "vn",
|
| 16 |
+
ko: "kr",
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
export function langToFlag(langCode: string): string {
|
| 20 |
+
return LANG_TO_FLAG[langCode] ?? langCode;
|
| 21 |
+
}
|
style.css
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
body {
|
| 2 |
-
padding: 2rem;
|
| 3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
h1 {
|
| 7 |
-
font-size: 16px;
|
| 8 |
-
margin-top: 0;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
p {
|
| 12 |
-
color: rgb(107, 114, 128);
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tsconfig.app.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "ES2023",
|
| 5 |
+
"useDefineForClassFields": true,
|
| 6 |
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
| 7 |
+
"module": "ESNext",
|
| 8 |
+
"types": ["vite/client"],
|
| 9 |
+
"skipLibCheck": true,
|
| 10 |
+
|
| 11 |
+
/* Bundler mode */
|
| 12 |
+
"moduleResolution": "bundler",
|
| 13 |
+
"allowImportingTsExtensions": true,
|
| 14 |
+
"verbatimModuleSyntax": true,
|
| 15 |
+
"moduleDetection": "force",
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
|
| 19 |
+
/* Linting */
|
| 20 |
+
"strict": true,
|
| 21 |
+
"noUnusedLocals": true,
|
| 22 |
+
"noUnusedParameters": true,
|
| 23 |
+
"erasableSyntaxOnly": true,
|
| 24 |
+
"noFallthroughCasesInSwitch": true,
|
| 25 |
+
"noUncheckedSideEffectImports": true
|
| 26 |
+
},
|
| 27 |
+
"include": ["src"]
|
| 28 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
tsconfig.node.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
| 4 |
+
"target": "ES2023",
|
| 5 |
+
"lib": ["ES2023"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"types": ["node"],
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"erasableSyntaxOnly": true,
|
| 22 |
+
"noFallthroughCasesInSwitch": true,
|
| 23 |
+
"noUncheckedSideEffectImports": true
|
| 24 |
+
},
|
| 25 |
+
"include": ["vite.config.ts"]
|
| 26 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from "vite";
|
| 2 |
+
import react from "@vitejs/plugin-react";
|
| 3 |
+
import tailwindcss from "@tailwindcss/vite";
|
| 4 |
+
|
| 5 |
+
// https://vite.dev/config/
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
plugins: [react(), tailwindcss()],
|
| 8 |
+
});
|