├── .eslintrc.cjs ├── .github └── FUNDING.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .stackblitzrc ├── LICENSE ├── README.md ├── eslint.config.mjs ├── i18n.config.ts ├── netlify.toml ├── nuxt.config.ts ├── package.json ├── pnpm-lock.yaml ├── scripts └── genLocaleKey.ts ├── server ├── api │ └── hello │ │ └── [name].ts ├── plugins │ └── nitroPlugin.ts ├── routes │ ├── hello.js │ └── hello.ts └── tsconfig.json ├── src ├── app.vue ├── assets │ └── css │ │ ├── globals.css │ │ └── index.css ├── components │ └── VThemeButton.vue ├── composables │ ├── index.ts │ └── useTrans.ts ├── config │ └── site.ts ├── constants │ └── index.ts ├── layouts │ ├── README.md │ └── default.vue ├── locales │ ├── en.ts │ ├── schema.ts │ └── zh_CN.ts ├── pages │ └── index.tsx ├── public │ ├── apple-touch-icon.png │ ├── avatar.jpg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── maskable-icon.png │ ├── nuxt.svg │ ├── pwa-192x192.png │ ├── pwa-512x512.png │ └── robots.txt ├── utils │ ├── clipboard.ts │ ├── css.ts │ └── date.ts └── workers │ └── sam.ts ├── tailwind.config.cjs └── tsconfig.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // require("@rushstack/eslint-patch/modern-module-resolution") 2 | 3 | module.exports = { 4 | extends: ["@nuxtjs/eslint-config-typescript"], 5 | rules: { 6 | quotes: ["error", "double", { allowTemplateLiterals: true }], 7 | "comma-dangle": ["error", "always-multiline"], 8 | "space-before-function-paren": "off", 9 | "arrow-parens": "off", 10 | "vue/valid-template-root": "off", 11 | "vue/no-multiple-template-root": "off", 12 | "vue/one-component-per-file": "off", 13 | "no-console": "off", 14 | "multiline-ternary": "off", 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hylarucoder 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | .output 5 | .nuxt 6 | .env 7 | .idea/ 8 | .DS_Store 9 | .vscode/ 10 | .pnpm-store/ 11 | src/public/models/sam/ 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | shell-emulator=true 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log 20 | .history 21 | CNAME 22 | /build 23 | /public 24 | .nuxt 25 | .output/ 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "prettier-plugin-tailwindcss" 4 | ], 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "semi": false, 8 | "singleQuote": false, 9 | "trailingComma": "all" 10 | } 11 | -------------------------------------------------------------------------------- /.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-PRESENT Hylarucoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Segment Anything WebGPU 2 | 3 | > In-browser image segmentation via Transformers.js 4 | 5 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { createConfigForNuxt } from "@nuxt/eslint-config/flat" 2 | 3 | export default createConfigForNuxt({ 4 | // options here 5 | }) 6 | -------------------------------------------------------------------------------- /i18n.config.ts: -------------------------------------------------------------------------------- 1 | import en from "./src/locales/en" 2 | import zhCN from "./src/locales/zh_CN" 3 | 4 | export default defineI18nConfig(() => ({ 5 | messages: { 6 | en, 7 | zh_CN: zhCN, 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NODE_VERSION = "18" 3 | 4 | [build] 5 | publish = "dist" 6 | command = "pnpm run build" 7 | 8 | [[redirects]] 9 | from = "/*" 10 | to = "/index.html" 11 | status = 200 12 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from "nuxt/config" 2 | import { appDescription } from "./src/constants" 3 | 4 | export default defineNuxtConfig({ 5 | srcDir: "src/", 6 | css: ["~/assets/css/index.css"], 7 | runtimeConfig: { 8 | public: { 9 | API_BASE_URL: process.env.NUXT_API_BASE_URL || "/api", 10 | }, 11 | }, 12 | modules: [ 13 | "@nuxt/image", 14 | "nuxt-icon", 15 | "@vueuse/nuxt", 16 | "@pinia/nuxt", 17 | "@nuxtjs/color-mode", 18 | "@nuxt/content", 19 | "@nuxt/ui", 20 | "@nuxtjs/tailwindcss", 21 | "@nuxt/eslint", 22 | ], 23 | tailwindcss: { 24 | configPath: "./tailwind.config.cjs", 25 | }, 26 | ui: { 27 | icons: ["mdi", "lucide", "vscode-icons", "fa6-brands"], 28 | }, 29 | image: { 30 | // Options 31 | }, 32 | experimental: { 33 | payloadExtraction: false, 34 | renderJsonPayloads: true, 35 | typedPages: true, 36 | }, 37 | 38 | colorMode: { 39 | classSuffix: "", 40 | }, 41 | 42 | nitro: { 43 | esbuild: { 44 | options: { 45 | target: "esnext", 46 | }, 47 | }, 48 | }, 49 | 50 | app: { 51 | head: { 52 | viewport: "width=device-width,initial-scale=1,user-scalable=no", 53 | link: [ 54 | { 55 | rel: "icon", 56 | href: "/favicon.ico", 57 | sizes: "any", 58 | }, 59 | { 60 | rel: "icon", 61 | type: "image/svg+xml", 62 | href: "/nuxt.svg", 63 | }, 64 | { 65 | rel: "apple-touch-icon", 66 | href: "/apple-touch-icon.png", 67 | }, 68 | { 69 | rel: "stylesheet", 70 | href: "https://rsms.me/inter/inter.css", 71 | }, 72 | ], 73 | script: [], 74 | meta: [ 75 | { 76 | name: "viewport", 77 | content: "width=device-width, initial-scale=1", 78 | }, 79 | { 80 | name: "description", 81 | content: appDescription, 82 | }, 83 | { 84 | name: "apple-mobile-web-app-status-bar-style", 85 | content: "black-translucent", 86 | }, 87 | ], 88 | }, 89 | }, 90 | content: { 91 | highlight: { 92 | // See the available themes on https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-theme 93 | theme: { 94 | dark: "github-dark", 95 | default: "github-light", 96 | }, 97 | }, 98 | }, 99 | postcss: { 100 | plugins: { 101 | tailwindcss: {}, 102 | autoprefixer: {}, 103 | }, 104 | }, 105 | i18n: { 106 | locales: ["en", "zh_CN"], 107 | defaultLocale: "en", 108 | strategy: "no_prefix", 109 | vueI18n: "./i18n.config.ts", // if you are using custom path, default 110 | }, 111 | 112 | // devtools: { 113 | // enabled: true, 114 | // }, 115 | ssr: false, 116 | vite: { 117 | build: { 118 | rollupOptions: { 119 | output: { 120 | experimentalMinChunkSize: 500_000, 121 | inlineDynamicImports: true, 122 | }, 123 | }, 124 | }, 125 | }, 126 | }) 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "packageManager": "pnpm@8.6.2", 5 | "scripts": { 6 | "prepare": "husky install", 7 | "build": "nuxi build", 8 | "dev": "nuxi dev --port 3000", 9 | "dev:pwa": "VITE_PLUGIN_PWA=true nuxi dev", 10 | "start": "node .output/server/index.mjs", 11 | "typecheck": "vue-tsc --noEmit", 12 | "lint": "eslint .", 13 | "lint:fix": "eslint . --fix", 14 | "postinstall": "nuxi prepare", 15 | "generate": "nuxi generate", 16 | "start:generate": "npx serve .output/public" 17 | }, 18 | "dependencies": { 19 | "@nuxt/content": "^2.12.1", 20 | "@nuxt/image": "1.5.0", 21 | "@nuxt/ui": "^2.15.2", 22 | "@nuxtjs/google-fonts": "^3.2.0", 23 | "@nuxtjs/tailwindcss": "^6.12.0", 24 | "@pinia/nuxt": "^0.5.1", 25 | "@vueuse/nuxt": "^10.9.0", 26 | "@xenova/transformers": "github:xenova/transformers.js#v3", 27 | "pinia": "^2.1.7", 28 | "shiki": "^1.3.0", 29 | "tailwindcss": "^3.4.3", 30 | "tailwindcss-animate": "^1.0.7", 31 | "ufo": "^1.5.3", 32 | "zod": "^3.23.0" 33 | }, 34 | "devDependencies": { 35 | "@egoist/tailwindcss-icons": "^1.7.4", 36 | "@iconify/json": "^2.2.202", 37 | "@nuxt/devtools": "^1.2.0", 38 | "@nuxt/eslint": "^0.3.8", 39 | "@nuxt/eslint-config": "^0.3.8", 40 | "@nuxtjs/eslint-config-typescript": "^12.1.0", 41 | "@nuxtjs/eslint-module": "^4.1.0", 42 | "@nuxtjs/i18n": "8.3.0", 43 | "@rushstack/eslint-patch": "^1.10.2", 44 | "@types/unist": "^3.0.2", 45 | "@typescript-eslint/parser": "^7.7.0", 46 | "@vue/eslint-config-prettier": "^9.0.0", 47 | "autoprefixer": "^10.4.19", 48 | "consola": "^3.2.3", 49 | "eslint": "^9.1.0", 50 | "eslint-plugin-nuxt": "^4.0.0", 51 | "husky": "^9.0.11", 52 | "lint-staged": "^15.2.2", 53 | "nuxt": "^3.11.2", 54 | "nuxt-icon": "^0.6.10", 55 | "postcss": "^8.4.38", 56 | "prettier": "^3.2.5", 57 | "prettier-plugin-tailwindcss": "^0.5.14", 58 | "tsx": "^4.7.2", 59 | "typescript": "^5.4.5", 60 | "vue-tsc": "^2.0.14" 61 | }, 62 | "lint-staged": { 63 | "*.{ts,tsx,js,jsx,mjs,vue}": "eslint --fix", 64 | "*.{ts,tsx,js,jsx,mjs,css,scss,less,md,vue}": "prettier --write" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /scripts/genLocaleKey.ts: -------------------------------------------------------------------------------- 1 | // write to file 2 | import fs from "fs" 3 | import path from "path" 4 | import en from "../src/locales/en" 5 | 6 | interface LocaleObject { 7 | [key: string]: LocaleObject | string 8 | } 9 | 10 | type TLocaleKey = keyof LocaleObject 11 | 12 | function getLocaleKeys(obj: LocaleObject, parentKey?: string): TLocaleKey[] { 13 | const keys: TLocaleKey[] = [] 14 | 15 | for (const key in obj) { 16 | const value = obj[key] 17 | 18 | if (typeof value === "object" && value !== null) { 19 | const nestedParentKey = parentKey ? `${parentKey}.${key}` : key 20 | const nestedKeys = getLocaleKeys(value, nestedParentKey) 21 | keys.push(...nestedKeys) 22 | } else { 23 | const fullKey = parentKey ? `${parentKey}.${key}` : key 24 | keys.push(fullKey as TLocaleKey) 25 | } 26 | } 27 | 28 | return keys 29 | } 30 | 31 | const keys = getLocaleKeys(en) 32 | const headerText = `\ 33 | const localeKeys = [ 34 | ` 35 | const middleText = keys.map((key) => ` "${key}",`).join("\n") 36 | const footerText = ` 37 | ] as const 38 | 39 | export type TLocaleKeys = (typeof localeKeys)[number] 40 | ` 41 | const __dirname = path.dirname(new URL(import.meta.url).pathname) 42 | 43 | const filePath = path.resolve(__dirname, "../src/locales/schema.ts") 44 | console.log("write to file:", filePath) 45 | fs.writeFileSync(filePath, headerText + middleText + footerText) 46 | -------------------------------------------------------------------------------- /server/api/hello/[name].ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler((event) => `Hello, ${event.context.params.name}!`) 2 | -------------------------------------------------------------------------------- /server/plugins/nitroPlugin.ts: -------------------------------------------------------------------------------- 1 | export default defineNitroPlugin((nitroApp) => { 2 | console.debug("Nitro plugin", nitroApp) 3 | }) 4 | -------------------------------------------------------------------------------- /server/routes/hello.js: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(() => { 2 | // console.log("Hello World!") 3 | }); 4 | -------------------------------------------------------------------------------- /server/routes/hello.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(() => { 2 | // console.log("Hello World!") 3 | }) 4 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/assets/css/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --theme: light; 3 | --white: #fff; 4 | --black: #303030; 5 | --gray: #fafafa; 6 | --primary: #42b883; 7 | --second: #e7f8ff; 8 | --hover-color: #f3f3f3; 9 | --bar-color: rgba(0, 0, 0, 0.1); 10 | --theme-color: var(--gray); 11 | --shadow: 50px 50px 100px 10px rgba(0, 0, 0, 0.1); 12 | --card-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.05); 13 | --border-in-light: 1px solid #dedede; 14 | --window-width: 90vw; 15 | --window-height: 90vh; 16 | --sidebar-width: 300px; 17 | --window-content-width: calc(100% - var(--sidebar-width)); 18 | --message-max-width: 80%; 19 | --full-height: 100%; 20 | --markdown-font-size: 16px; 21 | } 22 | 23 | html { 24 | font-family: ui-sans-serif, 25 | system-ui, 26 | -apple-system, 27 | BlinkMacSystemFont, 28 | Segoe UI, 29 | Roboto, 30 | Helvetica Neue, 31 | Arial, 32 | Noto Sans, 33 | sans-serif, 34 | Apple Color Emoji, 35 | Segoe UI Emoji, 36 | Segoe UI Symbol, 37 | Noto Color Emoji; 38 | font-feature-settings: normal; 39 | font-variation-settings: normal; 40 | } 41 | 42 | pre .copy-code-button { 43 | right: 10px; 44 | top: 15px; 45 | cursor: pointer; 46 | padding: 0 5px; 47 | text-align: right; 48 | background-color: var(--black); 49 | color: var(--white); 50 | border: var(--border-in-light); 51 | border-radius: 10px; 52 | transform: translateX(10px); 53 | pointer-events: none; 54 | opacity: 0.8; 55 | transition: all 0.3s ease; 56 | } 57 | 58 | .dark { 59 | --bg-color-900: #131313; 60 | --bg-color-800: #1e1e20; 61 | } 62 | 63 | html, 64 | body, 65 | #__nuxt { 66 | width: 100vw; 67 | min-height: 100vh; 68 | margin: 0; 69 | padding: 0; 70 | color: black; 71 | display: flex; 72 | background: #f5f5f5; 73 | } 74 | 75 | html.dark, 76 | .dark #__nuxt { 77 | @apply bg-zinc-950; 78 | color: #ebf4f1; 79 | } 80 | 81 | html.dark-theme { 82 | color: white; 83 | transition: background-color 0.5s ease, 84 | color 0.5s ease; 85 | } 86 | 87 | /* Hide scrollbar for Chrome, Safari and Opera */ 88 | .no-scrollbar::-webkit-scrollbar { 89 | display: none; 90 | } 91 | 92 | /* Hide scrollbar for IE, Edge and Firefox */ 93 | .no-scrollbar { 94 | -ms-overflow-style: none; /* IE and Edge */ 95 | scrollbar-width: none; /* Firefox */ 96 | } 97 | 98 | body, 99 | #container, 100 | #upload-button { 101 | display: flex; 102 | flex-direction: column; 103 | justify-content: center; 104 | align-items: center; 105 | } 106 | 107 | 108 | #container { 109 | position: relative; 110 | min-width: 800px; 111 | min-height: 600px; 112 | max-width: 100%; 113 | max-height: 100%; 114 | border: 2px dashed #D1D5DB; 115 | border-radius: 0.75rem; 116 | overflow: hidden; 117 | cursor: pointer; 118 | background-size: 100% 100%; 119 | background-position: center; 120 | background-repeat: no-repeat; 121 | } 122 | 123 | #mask-output { 124 | position: absolute; 125 | width: 100%; 126 | height: 100%; 127 | pointer-events: none; 128 | } 129 | 130 | 131 | #upload { 132 | display: none; 133 | } 134 | 135 | #example:hover { 136 | color: #2563EB; 137 | } 138 | 139 | canvas { 140 | position: absolute; 141 | width: 100%; 142 | height: 100%; 143 | opacity: 0.9; 144 | } 145 | 146 | 147 | .icon { 148 | height: 16px; 149 | width: 16px; 150 | position: absolute; 151 | transform: translate(-50%, -50%); 152 | } 153 | 154 | 155 | #information { 156 | margin-top: 0.25rem; 157 | font-size: 15px; 158 | } 159 | -------------------------------------------------------------------------------- /src/components/VThemeButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/nuxt-segment-anything-webgpu/9a6620ffa6803e595940b86a91e62a37e51221a6/src/composables/index.ts -------------------------------------------------------------------------------- /src/composables/useTrans.ts: -------------------------------------------------------------------------------- 1 | import { TLocale } from "~/locales/en" 2 | import { TLocaleKeys } from "~/locales/schema" 3 | 4 | export const useTrans = () => { 5 | const { t, n, locale, setLocale } = useI18n<{ message: TLocale }>({ 6 | useScope: "global", 7 | }) 8 | const trans = (key: TLocaleKeys, context?: any) => t(String(key), context) 9 | 10 | return { 11 | t: trans, 12 | n, 13 | locale, 14 | setLocale, 15 | } 16 | } 17 | 18 | export default useTrans 19 | -------------------------------------------------------------------------------- /src/config/site.ts: -------------------------------------------------------------------------------- 1 | export interface TProject { 2 | logo: string 3 | title: string 4 | description: string 5 | link: string 6 | source: string 7 | } 8 | 9 | export const projects: TProject[] = [ 10 | { 11 | logo: "https://chatgpt-nuxt.hylarucoder.io/nuxt.svg", 12 | title: "ChatGPT Nuxt", 13 | description: "One-Click to get well-designed cross-platform ChatGPT web UI.", 14 | link: "https://chatgpt-nuxt.hylarucoder.io", 15 | source: "hylarucoder.io", 16 | }, 17 | { 18 | logo: "https://chatgpt-nuxt.hylarucoder.io/nuxt.svg", 19 | title: "Open Source - tifa", 20 | description: "Yet another opinionated fastapi-start-kit with best practice", 21 | link: "https://github.com/hylarucoder/tifa", 22 | source: "github.com", 23 | }, 24 | { 25 | logo: "https://chatgpt-nuxt.hylarucoder.io/nuxt.svg", 26 | title: "Open Source - danmu.fm", 27 | description: "A command-line tool used to retrieve bullet screen messages for specified anchors on Douyu TV", 28 | link: "https://github.com/hylarucoder/danmu.fm", 29 | source: "github.com", 30 | }, 31 | { 32 | logo: "https://chatgpt-nuxt.hylarucoder.io/nuxt.svg", 33 | title: "Open Source - YaDjangoBlog", 34 | description: "Yet another django blog project with best practice", 35 | link: "https://github.com/hylarucoder/yadjangoblog", 36 | source: "github.com", 37 | }, 38 | ] 39 | 40 | const workExperience = [ 41 | { 42 | companyName: "Indie Hacker && Freelancer", 43 | jobTitle: "FullStack Engineer - WFH", 44 | startDate: "2023-05", 45 | endDate: "Present", 46 | imageUrl: "/logos/freelancer.jpeg", 47 | }, 48 | { 49 | companyName: "Kiwidrop.Inc", 50 | jobTitle: "FullStack Engineer - WFH", 51 | startDate: "2021-09", 52 | endDate: "2023-05", 53 | imageUrl: "/logos/kiwi.jpeg", 54 | }, 55 | { 56 | companyName: "上海食亨科技有限公司", 57 | jobTitle: "Backend Engineer", 58 | startDate: "2020-01", 59 | endDate: "2021-08", 60 | imageUrl: "/logos/shiheng.jpeg", 61 | }, 62 | { 63 | companyName: "荔枝微课 - 十方融海", 64 | jobTitle: "Tech Leader In 小爱项目组", 65 | startDate: "2019-10", 66 | endDate: "2020-08", 67 | imageUrl: "/logos/lizhi.png", 68 | }, 69 | { 70 | companyName: "上海报时树科技有限公司", 71 | jobTitle: "FullStack Engineer", 72 | startDate: "2018-03", 73 | endDate: "2019-10", 74 | imageUrl: "/logos/baoshishu.jpg", 75 | }, 76 | { 77 | companyName: "北京新中商数据科技有限公司", 78 | jobTitle: "FullStack Engineer", 79 | startDate: "2016-10", 80 | endDate: "2018-03", 81 | imageUrl: "/logos/zhongshang.jpeg", 82 | }, 83 | ] 84 | 85 | const socialLinks = [ 86 | { 87 | platform: "Twitter", 88 | url: "https://twitter.com/hylarucoder", 89 | icon: "i-fa6-brands-twitter text-blue-500 w-6 h-6 dark:bg-zinc-500", 90 | }, 91 | { 92 | platform: "GitHub", 93 | url: "https://github.com/hylarucoder", 94 | icon: "i-fa6-brands-github text-gray-600 w-6 h-6 dark:bg-zinc-500", 95 | }, 96 | { 97 | platform: "Youtube", 98 | url: "https://www.youtube.com/@hylarucoder", 99 | icon: "i-fa6-brands-youtube text-red-500 w-6 h-6 dark:bg-zinc-500", 100 | }, 101 | { 102 | platform: "LinkedIn", 103 | url: "https://www.linkedin.com/in/hylarucoder", 104 | icon: "i-fa6-brands-linkedin w-6 h-6 text-[#0962E5] dark:bg-zinc-500", 105 | }, 106 | { 107 | platform: "Bilibili", 108 | url: "https://space.bilibili.com/36269379", 109 | icon: "i-fa6-brands-bilibili text-[#0199d4] h-6 w-6 dark:bg-zinc-500", 110 | }, 111 | { 112 | platform: "Zhihu", 113 | url: "https://www.zhihu.com/people/hylarucoder", 114 | icon: "i-fa6-brands-zhihu text-[#0962E5] h-6 w-6 dark:bg-zinc-500", 115 | }, 116 | { 117 | platform: "Email", 118 | url: "mailto:twocucao@gmail.com", 119 | icon: "i-mdi-mail h-6 w-6 text-gray-600 dark:bg-zinc-500", 120 | }, 121 | ] 122 | export const siteConfig = { 123 | title: "HylaruCoder", 124 | description: "HylaruCoder's personal website", 125 | projects, 126 | workExperience, 127 | socialLinks, 128 | } 129 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const appName = "Nuxt Segment Anything WebGPU" 2 | export const appDescription = "Segment Anything WebGPU using Nuxt && Transform.js" 3 | -------------------------------------------------------------------------------- /src/layouts/README.md: -------------------------------------------------------------------------------- 1 | ## Layouts 2 | 3 | Vue components in this dir are used as layouts. 4 | 5 | By default, `default.vue` will be used unless an alternative is specified in the route meta. 6 | 7 | ```html 8 | 13 | ``` 14 | 15 | Learn more on https://nuxt.com/docs/guide/directory-structure/layouts 16 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/locales/en.ts: -------------------------------------------------------------------------------- 1 | const en = { 2 | WIP: "Coming Soon...", 3 | Home: { 4 | Title: "Home", 5 | }, 6 | } 7 | 8 | export type TLocale = typeof en 9 | export default en 10 | -------------------------------------------------------------------------------- /src/locales/schema.ts: -------------------------------------------------------------------------------- 1 | const localeKeys = [ 2 | "WIP", 3 | "Home.Title", 4 | "Home.Subtitle", 5 | "Articles.Title", 6 | "Articles.Subtitle", 7 | "Error.Unauthorized", 8 | "Auth.Title", 9 | "Auth.Tips", 10 | "Auth.Input", 11 | "Auth.Confirm", 12 | "Auth.Later", 13 | "ChatItem.ChatItemCount", 14 | ] as const 15 | 16 | export type TLocaleKeys = (typeof localeKeys)[number] 17 | -------------------------------------------------------------------------------- /src/locales/zh_CN.ts: -------------------------------------------------------------------------------- 1 | import { TLocale } from "./en" 2 | 3 | const cn: TLocale = { 4 | WIP: "该功能仍在开发中……", 5 | Home: { 6 | Title: "首页", 7 | }, 8 | } 9 | 10 | export default cn 11 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import SamWorker from "@/workers/sam?worker" 2 | import { ClientOnly, UButton, UContainer, UTable } from "#components" 3 | 4 | 5 | const BASE_URL = "https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/" 6 | const EXAMPLE_URL = BASE_URL + "corgi.jpg" 7 | 8 | const worker = new SamWorker() 9 | 10 | function clamp(x, min = 0, max = 1) { 11 | return Math.max(Math.min(x, max), min) 12 | } 13 | 14 | 15 | interface TPoint { 16 | label: number 17 | point: number[] 18 | } 19 | 20 | function sum(arr: number[]) { 21 | console.log(arr) 22 | return arr.reduce((acc, x) => acc + x, 0) 23 | } 24 | 25 | 26 | export default defineComponent({ 27 | setup(props) { 28 | const btnCutDisable = ref(true) 29 | const baseImage = ref("") 30 | const status = ref("") 31 | const refContainer = ref() 32 | const refMaskCanvas = ref() 33 | const lastPoints = ref([]) 34 | const isEncoded = ref(false) 35 | const isDecoding = ref(false) 36 | const isMultiMaskMode = ref(false) 37 | const modelReady = ref(false) 38 | const imageDataURI = ref("") 39 | const samReady = ref(false) 40 | const progressBarLog = ref([]) 41 | const samModelDLProgress = ref(0) 42 | 43 | const getMaskCanvas = () => { 44 | if (!refMaskCanvas.value) { 45 | throw Error("make") 46 | } 47 | return refMaskCanvas.value 48 | } 49 | 50 | 51 | onMounted(() => { 52 | // MaskCanvasSingleton.getInstance(refMaskCanvas?.value) 53 | }) 54 | 55 | 56 | function decode() { 57 | isDecoding.value = true 58 | worker.postMessage({ 59 | type: "decode", 60 | data: toRaw(lastPoints.value), 61 | }) 62 | } 63 | 64 | function getPoint(e) { 65 | const bb = refContainer.value.getBoundingClientRect() 66 | const mouseX = clamp((e.clientX - bb.left) / bb.width) 67 | const mouseY = clamp((e.clientY - bb.top) / bb.height) 68 | 69 | return { 70 | point: [mouseX, mouseY], 71 | label: e.button === 2 // right click 72 | ? 0 // negative prompt 73 | : 1, // positive prompt 74 | } 75 | } 76 | 77 | function clearPointsAndMask() { 78 | // Reset state 79 | isMultiMaskMode.value = false 80 | lastPoints.value = [] 81 | 82 | // Disable cut button 83 | btnCutDisable.value = true 84 | 85 | // Reset mask canvas 86 | const maskCanvas = getMaskCanvas() 87 | maskCanvas.getContext("2d").clearRect(0, 0, maskCanvas.width, maskCanvas.height) 88 | } 89 | 90 | function segment(data: string) { 91 | // Update state 92 | isEncoded.value = false 93 | if (!modelReady) { 94 | status.value = "Loading model..." 95 | } 96 | 97 | baseImage.value = data 98 | btnCutDisable.value = true 99 | imageDataURI.value = data 100 | 101 | worker.postMessage({ 102 | type: "segment", 103 | data, 104 | }) 105 | } 106 | 107 | worker.addEventListener("message", (e) => { 108 | const { 109 | type, 110 | data, 111 | } = e.data 112 | if (type === "ready") { 113 | modelReady.value = true 114 | status.value = "Ready" 115 | samReady.value = true 116 | } else if (type === "decode_result") { 117 | isDecoding.value = false 118 | 119 | if (!isEncoded) { 120 | return // We are not ready to decode yet 121 | } 122 | 123 | if (!isMultiMaskMode.value && lastPoints.value.length === 0) { 124 | // Perform decoding with the last point 125 | decode() 126 | lastPoints.value = [] 127 | } 128 | 129 | const { 130 | mask, 131 | scores, 132 | } = data 133 | 134 | const maskCanvas = getMaskCanvas() 135 | if (maskCanvas.width !== mask.width || maskCanvas.height !== mask.height) { 136 | maskCanvas.width = mask.width 137 | maskCanvas.height = mask.height 138 | } 139 | 140 | // Create context and allocate buffer for pixel data 141 | const context = maskCanvas.getContext("2d") 142 | const imageData = context.createImageData(maskCanvas.width, maskCanvas.height) 143 | 144 | // Select best mask 145 | const numMasks = scores.length // 3 146 | let bestIndex = 0 147 | for (let i = 1; i < numMasks; ++i) { 148 | if (scores[i] > scores[bestIndex]) { 149 | bestIndex = i 150 | } 151 | } 152 | status.value = `Segment score: ${scores[bestIndex].toFixed(2)}` 153 | 154 | // Fill mask with colour 155 | const pixelData = imageData.data 156 | for (let i = 0; i < pixelData.length; ++i) { 157 | if (mask.data[numMasks * i + bestIndex] === 1) { 158 | const offset = 4 * i 159 | pixelData[offset] = 0 // red 160 | pixelData[offset + 1] = 114 // green 161 | pixelData[offset + 2] = 189 // blue 162 | pixelData[offset + 3] = 255 // alpha 163 | } 164 | } 165 | 166 | // Draw image data to context 167 | context.putImageData(imageData, 0, 0) 168 | 169 | } else if (type === "segment_result") { 170 | if (data === "start") { 171 | status.value = "Extracting image embedding..." 172 | } else { 173 | status.value = "Embedding extracted!" 174 | isEncoded.value = true 175 | } 176 | } else if (type === "sam_model_download") { 177 | progressBarLog.value = data as any[] 178 | samModelDLProgress.value = (sum(data.map(x => { 179 | return x.progress 180 | })) / progressBarLog.value.length).toFixed(2) 181 | } 182 | }) 183 | 184 | 185 | return () => ( 186 | 187 | 188 |

Nuxt Segment Anything WebGPU

189 |

190 | In-browser image segmentation via {" "} 191 | 192 | Transformers.js 193 | 194 |

195 |
{ 197 | if (baseImage.value) { 198 | e.preventDefault() 199 | } 200 | }} 201 | onMousemove={(e) => { 202 | if (!isEncoded.value || isMultiMaskMode.value) { 203 | // Ignore mousemove events if the image is not encoded yet, 204 | // or we are in multi-mask mode 205 | return 206 | } 207 | lastPoints.value = [getPoint(e)] 208 | 209 | if (!isDecoding.value) { 210 | decode() // Only decode if we are not already decoding 211 | } 212 | }} 213 | ref={refContainer} 214 | style={{ 215 | backgroundImage: baseImage.value ? `url(${baseImage.value})` : "none", 216 | }} 217 | onMousedown={(e) => { 218 | e.preventDefault() 219 | if (e.button !== 0 && e.button !== 2) { 220 | return // Ignore other buttons 221 | } 222 | if (!isEncoded.value) { 223 | return // Ignore if not encoded yet 224 | } 225 | if (!isMultiMaskMode.value) { 226 | lastPoints.value = [] 227 | isMultiMaskMode.value = true 228 | btnCutDisable.value = false 229 | } 230 | if (!baseImage.value) { 231 | return 232 | } 233 | 234 | const point = getPoint(e) 235 | lastPoints.value.push(point) 236 | decode() 237 | }} 238 | > 239 | 258 | 259 | { 260 | lastPoints.value.map((point) => { 261 | return <> 262 | { 263 | point.label === 1 ? : 267 | 271 | } 272 | 273 | }) 274 | } 275 | { 276 | (!isEncoded.value && baseImage.value) && 277 |
278 |
279 |
loading model, please wait for a moment
280 | {/* calculate progress */} 281 | { 282 | samModelDLProgress.value 283 | } % 284 |
285 |
286 | } 287 |
288 | 289 |
290 | { 293 | // Update state 294 | isEncoded.value = false 295 | imageDataURI.value = "" 296 | 297 | worker.postMessage({ type: "reset" }) 298 | 299 | // Clear points and mask (if present) 300 | clearPointsAndMask() 301 | 302 | // Update UI 303 | btnCutDisable.value = true 304 | baseImage.value = "" 305 | status.value = "Ready" 306 | }} 307 | >Reset image 308 | 309 | { 310 | clearPointsAndMask() 311 | }}>Clear points 312 | 313 | { 317 | // Update canvas dimensions (if different) 318 | const maskCanvas = getMaskCanvas() 319 | const [w, h] = [maskCanvas.width, maskCanvas.height] 320 | 321 | // Get the mask pixel data 322 | const maskContext = maskCanvas.getContext("2d") 323 | const maskPixelData = maskContext.getImageData(0, 0, w, h) 324 | // Load the image 325 | const image = new Image() 326 | image.crossOrigin = "anonymous" 327 | image.onload = async () => { 328 | // Create a new canvas to hold the image 329 | const imageCanvas = new OffscreenCanvas(w, h) 330 | const imageContext = imageCanvas.getContext("2d") 331 | imageContext!.drawImage(image, 0, 0, w, h) 332 | const imagePixelData = imageContext!.getImageData(0, 0, w, h) 333 | 334 | // Create a new canvas to hold the cut-out 335 | const cutCanvas = new OffscreenCanvas(w, h) 336 | const cutContext = cutCanvas.getContext("2d") 337 | const cutPixelData = cutContext!.getImageData(0, 0, w, h) 338 | 339 | // Copy the image pixel data to the cut canvas 340 | for (let i = 3; i < maskPixelData.data.length; i += 4) { 341 | if (maskPixelData.data[i] > 0) { 342 | for (let j = 0; j < 4; ++j) { 343 | const offset = i - j 344 | cutPixelData.data[offset] = imagePixelData.data[offset] 345 | } 346 | } 347 | } 348 | cutContext!.putImageData(cutPixelData, 0, 0) 349 | 350 | // Download image 351 | const link = document.createElement("a") 352 | link.download = "image.png" 353 | link.href = URL.createObjectURL(await cutCanvas.convertToBlob()) 354 | link.click() 355 | link.remove() 356 | } 357 | image.src = imageDataURI.value 358 | console.log("image.src", imageDataURI) 359 | }} 360 | >Cut Mask 361 | 362 |
363 |

364 | Left click = positive points, right click = negative points. 365 |

366 | { 367 | const file = e.target.files[0] 368 | if (!file) { 369 | return 370 | } 371 | 372 | const reader = new FileReader() 373 | 374 | // Set up a callback when the file is loaded 375 | reader.onload = e2 => segment(e2.target.result) 376 | 377 | reader.readAsDataURL(file) 378 | 379 | }} 380 | /> 381 |
382 | 383 |
384 | ) 385 | }, 386 | }) 387 | 388 | -------------------------------------------------------------------------------- /src/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/nuxt-segment-anything-webgpu/9a6620ffa6803e595940b86a91e62a37e51221a6/src/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/public/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/nuxt-segment-anything-webgpu/9a6620ffa6803e595940b86a91e62a37e51221a6/src/public/avatar.jpg -------------------------------------------------------------------------------- /src/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/nuxt-segment-anything-webgpu/9a6620ffa6803e595940b86a91e62a37e51221a6/src/public/favicon-16x16.png -------------------------------------------------------------------------------- /src/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/nuxt-segment-anything-webgpu/9a6620ffa6803e595940b86a91e62a37e51221a6/src/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/nuxt-segment-anything-webgpu/9a6620ffa6803e595940b86a91e62a37e51221a6/src/public/favicon.ico -------------------------------------------------------------------------------- /src/public/maskable-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/nuxt-segment-anything-webgpu/9a6620ffa6803e595940b86a91e62a37e51221a6/src/public/maskable-icon.png -------------------------------------------------------------------------------- /src/public/nuxt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 9 | 12 | 14 | 17 | 20 | 22 | 25 | 28 | 31 | 34 | 37 | 40 | 43 | 46 | 49 | 52 | 54 | 57 | 59 | 62 | 65 | 66 | 69 | 72 | 75 | 78 | 81 | 84 | 86 | 89 | 92 | 95 | 97 | 100 | 102 | 105 | 108 | 111 | 113 | 115 | 117 | 118 | 121 | 124 | 127 | 130 | 133 | 136 | 139 | 140 | 143 | 146 | 149 | 152 | 155 | 158 | 161 | 164 | 167 | 170 | 173 | 176 | 179 | 182 | 185 | 188 | 191 | 194 | 197 | 200 | 203 | 206 | 209 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /src/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/nuxt-segment-anything-webgpu/9a6620ffa6803e595940b86a91e62a37e51221a6/src/public/pwa-192x192.png -------------------------------------------------------------------------------- /src/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hylarucoder/nuxt-segment-anything-webgpu/9a6620ffa6803e595940b86a91e62a37e51221a6/src/public/pwa-512x512.png -------------------------------------------------------------------------------- /src/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /src/utils/clipboard.ts: -------------------------------------------------------------------------------- 1 | export const copyToClipboard = (content: string) => { 2 | const input = document.createElement("textarea") 3 | input.innerHTML = content 4 | input.setAttribute("readonly", "readonly") 5 | input.setAttribute("value", content) 6 | document.body.appendChild(input) 7 | input.select() 8 | input.setSelectionRange(0, 99999) 9 | document.execCommand("copy") 10 | document.body.removeChild(input) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/css.ts: -------------------------------------------------------------------------------- 1 | export const setMobileCssVariables = (attrs: Record) => { 2 | for (const attr in attrs) { 3 | document.documentElement.style.setProperty(`${attr}`, `${attrs[attr]}`) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | // Date utilities 2 | export function getUtcNow() { 3 | return new Date().toISOString().toString() 4 | } 5 | 6 | export function formatDateString(dateString: string): string { 7 | const date = new Date(dateString) 8 | const now = new Date() 9 | 10 | const timeDifference = now.getTime() - date.getTime() 11 | const withinOneDay = timeDifference < 24 * 60 * 60 * 1000 12 | const withinOneYear = now.getFullYear() - date.getFullYear() < 1 13 | 14 | const hours = date.getHours().toString().padStart(2, "0") 15 | const minutes = date.getMinutes().toString().padStart(2, "0") 16 | const day = date.getDate().toString().padStart(2, "0") 17 | const month = (date.getMonth() + 1).toString().padStart(2, "0") 18 | const year = date.getFullYear() 19 | 20 | if (withinOneDay) { 21 | return `${hours}:${minutes}` 22 | } else if (withinOneYear) { 23 | return `${month}/${day} ${hours}:${minutes}` 24 | } else { 25 | return `${year}/${month}/${day}` 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/workers/sam.ts: -------------------------------------------------------------------------------- 1 | import { SamModel, AutoProcessor, RawImage, Tensor, env } from "@xenova/transformers" 2 | 3 | // env.localModelPath = "/models/" 4 | // env.allowRemoteModels = false 5 | // env.allowLocalModels = true 6 | 7 | 8 | // We adopt the singleton pattern to enable lazy-loading of the model and processor. 9 | export class SegmentAnythingSingleton { 10 | // static model_id = "sam/slimsam-77-uniform" 11 | static model_id = "Xenova/slimsam-77-uniform" 12 | static model: SamModel 13 | static processor: AutoProcessor 14 | 15 | static getInstance(progress_callback) { 16 | if (!this.model) { 17 | this.model = SamModel.from_pretrained(this.model_id, { 18 | dtype: { 19 | vision_encoder: "fp16", 20 | prompt_encoder_mask_decoder: "q8", 21 | }, 22 | device: { 23 | vision_encoder: "webgpu", 24 | prompt_encoder_mask_decoder: "wasm", 25 | }, 26 | progress_callback: (progress) => { 27 | progress.from = "SamModel" 28 | progress_callback(progress) 29 | }, 30 | }) 31 | } 32 | if (!this.processor) { 33 | this.processor = AutoProcessor.from_pretrained( 34 | this.model_id, 35 | { 36 | progress_callback: (progress) => { 37 | progress.from = "AutoProcessor" 38 | progress_callback(progress) 39 | }, 40 | }, 41 | ) 42 | } 43 | 44 | return Promise.all([this.model, this.processor]) 45 | } 46 | } 47 | 48 | // State variables 49 | let imageEmbeddings = null 50 | let imageInputs = null 51 | let ready = false 52 | let readyNess = { 53 | "config.json": 0, 54 | "onnx/prompt_encoder_mask_decoder_quantized.onnx": 0, 55 | "onnx/vision_encoder_fp16.onnx": 0, 56 | "preprocessor_config.json": 0, 57 | } 58 | const samModelDownloadProgress = [ 59 | { 60 | file: "config.json", 61 | progress: 0, 62 | status: "initial", 63 | }, 64 | { 65 | file: "onnx/prompt_encoder_mask_decoder_quantized.onnx", 66 | progress: 0, 67 | status: "initial", 68 | }, 69 | { 70 | file: "onnx/vision_encoder_fp16.onnx", 71 | progress: 0, 72 | status: "initial", 73 | }, 74 | { 75 | file: "preprocessor_config.json", 76 | progress: 0, 77 | status: "initial", 78 | }, 79 | ] 80 | 81 | const computeProgress = (step) => { 82 | let progress = step.progress || 0 83 | if (step.status === "initial") { 84 | progress = 0 85 | } 86 | if (step.status === "done") { 87 | progress = 100 88 | } 89 | return { 90 | file: step.file, 91 | progress, 92 | status: step.status, 93 | } 94 | } 95 | 96 | self.onmessage = async (e) => { 97 | const [model, processor] = await SegmentAnythingSingleton.getInstance((progress) => { 98 | const targetProgress = samModelDownloadProgress.find((x) => x.file === progress.file) as any 99 | const fileProgress = computeProgress(progress) 100 | targetProgress.file = fileProgress.file 101 | targetProgress.progress = fileProgress.progress 102 | targetProgress.status = fileProgress.status 103 | self.postMessage({ 104 | type: "sam_model_download", 105 | data: samModelDownloadProgress, 106 | }) 107 | }) 108 | if (!ready) { 109 | ready = true 110 | self.postMessage({ 111 | type: "ready", 112 | }) 113 | } 114 | 115 | const { type, data } = e.data 116 | if (type === "reset") { 117 | imageInputs = null 118 | imageEmbeddings = null 119 | 120 | } else if (type === "segment") { 121 | // Indicate that we are starting to segment the image 122 | self.postMessage({ 123 | type: "segment_result", 124 | data: "start", 125 | }) 126 | 127 | // Read the image and recompute image embeddings 128 | const image = await RawImage.read(e.data.data) 129 | imageInputs = await processor(image) 130 | imageEmbeddings = await model.get_image_embeddings(imageInputs) 131 | 132 | // Indicate that we have computed the image embeddings, and we are ready to accept decoding requests 133 | self.postMessage({ 134 | type: "segment_result", 135 | data: "done", 136 | }) 137 | 138 | } else if (type === "decode") { 139 | // Prepare inputs for decoding 140 | const reshaped = imageInputs.reshaped_input_sizes[0] 141 | const points = data.map(x => [x.point[0] * reshaped[1], x.point[1] * reshaped[0]]) 142 | const labels = data.map(x => BigInt(x.label)) 143 | 144 | const input_points = new Tensor( 145 | "float32", 146 | points.flat(Infinity), 147 | [1, 1, points.length, 2], 148 | ) 149 | const input_labels = new Tensor( 150 | "int64", 151 | labels.flat(Infinity), 152 | [1, 1, labels.length], 153 | ) 154 | 155 | // Generate the mask 156 | const { pred_masks, iou_scores } = await model({ 157 | ...imageEmbeddings, 158 | input_points, 159 | input_labels, 160 | }) 161 | 162 | // Post-process the mask 163 | const masks = await processor.post_process_masks( 164 | pred_masks, 165 | imageInputs.original_sizes, 166 | imageInputs.reshaped_input_sizes, 167 | ) 168 | 169 | // Send the result back to the main thread 170 | self.postMessage({ 171 | type: "decode_result", 172 | data: { 173 | mask: RawImage.fromTensor(masks[0][0]), 174 | scores: iou_scores.data, 175 | }, 176 | }) 177 | 178 | } else { 179 | throw new Error(`Unknown message type: ${type}`) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require("tailwindcss/defaultTheme") 2 | 3 | /** @type {import("tailwindcss").Config} */ 4 | module.exports = { 5 | darkMode: ["class"], 6 | content: [ 7 | "./src/**/*.{js,vue,ts,tsx}", 8 | ], 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | sans: ["Inter var", ...fontFamily.sans], 13 | }, 14 | }, 15 | }, 16 | plugins: [ 17 | // require("tailwindcss-animate"), 18 | // require("@tailwindcss/forms"), 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------