Upload demo files

#1
by Xenova HF Staff - opened
.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
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
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
+ });