├── .gitignore
├── .yarnrc.yml
├── README.md
├── app.config.ts
├── app.vue
├── components
├── Clicker.vue
├── Demo.vue
├── Hero.vue
└── Uploader.vue
├── nuxt.config.ts
├── package.json
├── pages
└── index.vue
├── prettier.config.cjs
├── public
├── cover.jpg
├── cross-icon.png
├── demo
│ ├── bees.mp4
│ ├── dog.mp4
│ ├── fountain.mp4
│ └── jellyfish.mp4
├── favicon.png
├── logo.png
└── star-icon.png
├── server
├── api
│ ├── file.post.js
│ ├── prediction.get.js
│ └── prediction.post.js
└── tsconfig.json
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Nuxt dev/build outputs
2 | .output
3 | .data
4 | .nuxt
5 | .nitro
6 | .cache
7 | dist
8 |
9 | # Node dependencies
10 | node_modules
11 |
12 | # Logs
13 | logs
14 | *.log
15 |
16 | # Misc
17 | .DS_Store
18 | .fleet
19 | .idea
20 | .yarn
21 |
22 | # Local env files
23 | .env
24 | .env.*
25 | !.env.example
26 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Green Screen Creator
2 |
3 | Track objects in videos and add a green screen to the background.
4 |
5 | Powered by Meta's [Segment Anything Model (SAM)](https://replicate.com/meta/sam-2-video) on Replicate.
6 |
7 | ## Setup
8 |
9 | Make sure to install the dependencies:
10 |
11 | ```bash
12 | # npm
13 | npm install
14 |
15 | # pnpm
16 | pnpm install
17 |
18 | # yarn
19 | yarn install
20 |
21 | # bun
22 | bun install
23 | ```
24 |
25 | ## Development Server
26 |
27 | Start the development server on `http://localhost:3000`:
28 |
29 | ```bash
30 | # npm
31 | npm run dev
32 |
33 | # pnpm
34 | pnpm run dev
35 |
36 | # yarn
37 | yarn dev
38 |
39 | # bun
40 | bun run dev
41 | ```
42 |
--------------------------------------------------------------------------------
/app.config.ts:
--------------------------------------------------------------------------------
1 | export default defineAppConfig({
2 | ui: {
3 | primary: 'green',
4 | container: {
5 | constrained: 'max-w-5xl'
6 | }
7 | }
8 | })
9 |
--------------------------------------------------------------------------------
/app.vue:
--------------------------------------------------------------------------------
1 |
2 | #app
3 | nuxt-page
4 |
5 |
6 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/components/Clicker.vue:
--------------------------------------------------------------------------------
1 |
2 | u-card(
3 | :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
4 | )
5 | template(#header)
6 | .flex.items-center.justify-between
7 | h3.text-base.font-semibold.leading-6.text-gray-900(
8 | class="dark:text-white"
9 | ) Click on objects to track
10 | u-button(
11 | @click="$emit('reset')"
12 | :disabled="loading"
13 | color="gray"
14 | variant="ghost"
15 | icon="i-heroicons-x-mark-20-solid"
16 | class="-my-1"
17 | )
18 | .relative
19 | div.w-full.aspect-1.bg-contain.bg-center.bg-no-repeat.bg-black.cursor-pointer(
20 | @mousemove="onMouseMove"
21 | @mousedown="onMouseDown"
22 | @contextmenu="(e) => e.preventDefault()"
23 | ref="container"
24 | )
25 | canvas.absolute.w-full.h-full.pointer-events-none.opacity-60(ref="maskCanvas")
26 | .hidden
27 | img.icon(
28 | ref="starIcon"
29 | src="/star-icon.png"
30 | )
31 | img.icon(
32 | ref="crossIcon"
33 | src="/cross-icon.png"
34 | )
35 | .pt-6.text-sm.grid.grid-cols-3
36 | .flex.items-center
37 | u-kbd(
38 | value="Left click = retain"
39 | )
40 | u-divider(orientation="vertical")
41 | .flex.items-center
42 | u-kbd(
43 | value="Right click = green screen"
44 | )
45 | template(#footer)
46 | .flex
47 | u-button(
48 | @click="reset"
49 | :disabled="loading"
50 | color="red"
51 | variant="link"
52 | ) Clear Points
53 | .grow
54 | u-button(
55 | @click="submit"
56 | :loading="loading"
57 | :disabled="false"
58 | color="black"
59 | ) Submit
60 |
61 |
62 |
293 |
294 |
302 |
--------------------------------------------------------------------------------
/components/Demo.vue:
--------------------------------------------------------------------------------
1 |
2 | .mx-auto.px-4.max-w-5xl(
3 | class="sm:pb-16 md:pb-24 sm:px-6 lg:px-8"
4 | )
5 | .text-center(
6 | class="sm:pb-12 md:pb-20"
7 | )
8 | h1.text-4xl.font-bold.tracking-tight.text-gray-900(
9 | class="sm:text-6xl"
10 | ) Demos
11 | video.w-full.rounded-2xl.mt-12(
12 | v-for="(demo, i) in demos"
13 | :key="`demo-${i}`"
14 | crossorigin="anonymous"
15 | controls
16 | )
17 | source(:src="`/demo/${demo}.mp4`")
18 |
19 |
20 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/components/Hero.vue:
--------------------------------------------------------------------------------
1 |
2 | .pt-8.pb-8.px-4.relative(
3 | class="sm:pb-16 md:pb-24 sm:px-6 lg:px-8"
4 | )
5 |
6 | //- API token modal
7 | u-modal(v-model="modal")
8 | u-card(
9 | :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
10 | )
11 | template(#header)
12 | .flex.items-center.justify-between
13 | h3.text-base.font-semibold.leading-6.text-gray-900(
14 | class="dark:text-white"
15 | ) Replicate API token
16 | u-button(
17 | color="gray"
18 | variant="ghost"
19 | icon="i-heroicons-x-mark-20-solid"
20 | class="-my-1"
21 | @click="modal = false"
22 | )
23 | p.text-sm.text-gray-500(
24 | class="dark:text-gray-400"
25 | )
26 | | Provide your personal Replicate API token. We will store the token in your browser's
27 | a(
28 | href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"
29 | target="_blank"
30 | ) localstorage
31 | | , and use it to run machine learning models in the cloud.
32 | template(#footer)
33 | u-button(
34 | to="https://replicate.com/account/api-tokens"
35 | target="_blank"
36 | color="black"
37 | icon="i-heroicons-arrow-top-right-on-square"
38 | trailing
39 | ) My Replicate API tokens
40 |
41 | .mb-8.mx-auto.gap-4.flex.flex-row.max-w-5xl.items-center(
42 | class="sm:mb-16 md:mb-24"
43 | )
44 | img.max-h-8(src="/logo.png")
45 | .text-base(
46 | class="md:text-2xl"
47 | )
48 | span.text-primary green
49 | | screen
50 | span.text-primary creator
51 | .grow
52 | u-button(
53 | to="https://github.com/replicate/green-screen-creator"
54 | target="_blank"
55 | size="lg"
56 | icon="i-heroicons-code-bracket"
57 | variant="ghost"
58 | ) Get the Code
59 | .mx-auto.gap-16.flex.flex-col.max-w-4xl(
60 | class="sm:gap-y-24"
61 | )
62 | .text-center.relative
63 | h1.text-5xl.font-bold.tracking-tight.text-gray-900(
64 | class="dark:text-white sm:text-7xl"
65 | )
66 | span.text-primary Green
67 | | Screen
68 | span.text-primary Creator
69 | p.mt-6.mb-10.text-lg.tracking-tight.text-gray-600(
70 | class="dark:text-gray-100"
71 | )
72 | | Track an object in a video and add a green screen to the background.
73 | .flex.justify-center.space-x-2
74 | u-input(
75 | v-model="api_token"
76 | @keydown.enter=""
77 | size="xl"
78 | placeholder="Replicate API token"
79 | icon="i-heroicons-key"
80 | type="password"
81 | )
82 | u-button(
83 | @click=""
84 | :disabled="!api_token"
85 | size="xl"
86 | color="black"
87 | ) Set token
88 | u-button(
89 | @click="modal = true"
90 | icon="i-heroicons-question-mark-circle"
91 | variant="link"
92 | size="xl"
93 | )
94 |
95 |
96 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/components/Uploader.vue:
--------------------------------------------------------------------------------
1 |
2 | #uploader.mx-auto.pb-8.px-4.max-w-5xl.grid.grid-cols-1.gap-8(
3 | v-if="api_token"
4 | class="sm:pb-16 md:pb-24 sm:px-6 lg:px-8 sm:gap-y-24 md:grid-cols-2"
5 | )
6 | //- Hidden input
7 | input(
8 | @change="onFileSelected"
9 | type="file"
10 | ref="file"
11 | accept="video/*"
12 | )
13 |
14 | //- First-frame modal
15 | u-modal(
16 | v-model="modal"
17 | prevent-close
18 | )
19 | clicker(
20 | @submit="submit"
21 | @reset="modal = false; reset();"
22 | :image="input_video_first_frame"
23 | :loading="loading_upload"
24 | )
25 |
26 | div
27 | //- File upload
28 | template(v-if="input_method === 'file'")
29 | .upload-button.relative.overflow-hidden.w-full.flex.flex-col.gap-4.justify-center.items-center.text-center.cursor-pointer.rounded-2xl.aspect-1.text-2xl(
30 | @click="onClickSelect"
31 | class="bg-gray-100 hover:bg-[#dddddd]"
32 | )
33 | template(v-if="input_video_object_url")
34 | .preview
35 | video.w-full.h-full.bg-black(controls)
36 | source(:src="input_video_object_url")
37 | template(v-else)
38 | template(v-if="loading_file")
39 | u-progress(
40 | :ui="{ wrapper: 'w-9/12' }"
41 | animation="swing"
42 | )
43 | template(v-else)
44 | u-icon.icon(
45 | name="i-heroicons-arrow-up-tray"
46 | )
47 | span Upload a video
48 | .text-center.my-4(v-if="input_video_object_url")
49 | u-button(
50 | @click="reset"
51 | icon="i-heroicons-trash"
52 | variant="link"
53 | color="black"
54 | )
55 | .text-center.my-4(v-else)
56 | u-button(
57 | @click="input_method = 'url'"
58 | icon="i-heroicons-link"
59 | variant="link"
60 | color="black"
61 | ) or paste an URL of a video file
62 |
63 | //- URL upload
64 | template(v-if="input_method === 'url'")
65 | .upload-button.relative.overflow-hidden.w-full.flex.flex-col.gap-4.justify-center.items-center.text-center.rounded-2xl.aspect-1.text-2xl(
66 | class="bg-gray-100"
67 | )
68 | u-input(
69 | v-model="input_video_url"
70 | @keydown.enter="onClickSelectUrl"
71 | :disabled="loading_file"
72 | class="w-9/12"
73 | size="xl"
74 | placeholder="https://"
75 | autofocus
76 | )
77 | u-button(
78 | @click="onClickSelectUrl"
79 | :loading="loading_file"
80 | class="w-9/12"
81 | size="xl"
82 | color="black"
83 | block
84 | ) Upload
85 | .text-center.my-4
86 | u-button(
87 | @click="input_method = 'file'"
88 | icon="i-heroicons-arrow-up-tray"
89 | variant="link"
90 | color="black"
91 | ) or upload a file from your computer
92 |
93 | div.space-y-4
94 | template(v-if="loading_prediction")
95 | template(v-if="!output_video")
96 | u-divider.mb-4(
97 | v-if="status"
98 | :label="status"
99 | type="dashed"
100 | )
101 | u-alert.mb-4(
102 | title="The selected object in the video is being tracked"
103 | description="This is done by the SAM 2 model that is running on Replicate."
104 | icon="i-heroicons-video-camera"
105 | :actions="[{ variant: 'solid', color: 'primary', label: 'Run SAM 2 with an API', icon: 'i-heroicons-arrow-top-right-on-square', trailing: true, click: openUrl }]"
106 | color="primary"
107 | variant="soft"
108 | )
109 | video.w-full.bg-black.aspect-1.rounded-2xl(
110 | v-else
111 | controls
112 | crossorigin="anonymous"
113 | )
114 | source(:src="output_video")
115 |
116 |
117 |
559 |
560 |
584 |
--------------------------------------------------------------------------------
/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtConfig({
2 | compatibilityDate: '2024-04-03',
3 | runtimeConfig: {
4 | public: {}
5 | },
6 | devtools: { enabled: false },
7 | ssr: false,
8 | nitro: {
9 | preset: 'vercel',
10 | routeRules: {
11 | '/**': {
12 | headers: {
13 | 'Cross-Origin-Embedder-Policy': 'require-corp',
14 | 'Cross-Origin-Opener-Policy': 'same-origin'
15 | }
16 | }
17 | }
18 | },
19 | sourcemap: {
20 | server: false,
21 | client: false
22 | },
23 | app: {
24 | head: {
25 | htmlAttrs: {
26 | class: 'h-full'
27 | },
28 | title: 'Green Screen Creator',
29 | link: [
30 | { rel: 'canonical', href: 'https://green-screen-creator.vercel.app' },
31 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.png' }
32 | ],
33 | meta: [
34 | { hid: 'charset', charset: 'utf-8' },
35 | { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' },
36 | {
37 | hid: 'viewport',
38 | name: 'viewport',
39 | content:
40 | 'width=device-width,height=device-height,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0,viewport-fit=cover'
41 | },
42 | {
43 | hid: 'format-detection',
44 | name: 'format-detection',
45 | content: 'telephone=no'
46 | },
47 | {
48 | hid: 'description',
49 | name: 'description',
50 | content:
51 | 'Track an object in a video and add a green screen to the background.'
52 | },
53 | {
54 | hid: 'og:type',
55 | name: 'og:type',
56 | property: 'og:type',
57 | content: 'website'
58 | },
59 | {
60 | hid: 'og:url',
61 | name: 'og:url',
62 | property: 'og:url',
63 | content: 'https://green-screen-creator.vercel.app'
64 | },
65 | {
66 | hid: 'og:site_name',
67 | name: 'og:site_name',
68 | property: 'og:site_name',
69 | content: 'green-screen-creator.vercel.app'
70 | },
71 | {
72 | hid: 'og:title',
73 | name: 'og:title',
74 | property: 'og:title',
75 | content: 'Green Screen Creator'
76 | },
77 | {
78 | hid: 'og:description',
79 | name: 'og:description',
80 | property: 'og:description',
81 | content:
82 | 'Track an object in a video and add a green screen to the background.'
83 | },
84 | {
85 | hid: 'og:image',
86 | name: 'og:image',
87 | property: 'og:image',
88 | content: 'https://green-screen-creator.vercel.app/cover.jpg'
89 | },
90 | {
91 | name: 'twitter:card',
92 | content: 'summary_large_image'
93 | },
94 | {
95 | name: 'twitter:title',
96 | content: 'Green Screen Creator'
97 | },
98 | {
99 | name: 'twitter:description',
100 | content:
101 | 'Track an object in a video and add a green screen to the background.'
102 | },
103 | {
104 | name: 'twitter:image',
105 | content: 'https://green-screen-creator.vercel.app/cover.jpg'
106 | },
107 | {
108 | hid: 'msapplication-TileColor',
109 | name: 'msapplication-TileColor',
110 | content: '#ffffff'
111 | },
112 | { hid: 'theme-color', name: 'theme-color', content: '#ffffff' },
113 | {
114 | hid: 'mobile-web-app-capable',
115 | name: 'mobile-web-app-capable',
116 | content: 'yes'
117 | },
118 | {
119 | hid: 'apple-mobile-web-app-title',
120 | name: 'apple-mobile-web-app-title',
121 | content: 'green-screen-creator.vercel.app'
122 | }
123 | ],
124 | script: [
125 | {
126 | async: true,
127 | src: `https://www.googletagmanager.com/gtag/js?id=${process.env.GTAG_ID}`
128 | },
129 | {
130 | children: `
131 | window.dataLayer = window.dataLayer || [];
132 | function gtag(){dataLayer.push(arguments);}
133 | gtag('js', new Date());
134 | gtag('config', '${process.env.GTAG_ID}');`
135 | }
136 | ],
137 | bodyAttrs: {
138 | class: 'antialiased h-full min-h-screen relative'
139 | }
140 | }
141 | },
142 | modules: ['@nuxt/ui', '@vueuse/nuxt'],
143 | colorMode: {
144 | preference: 'light'
145 | },
146 | ui: {
147 | global: true
148 | }
149 | })
150 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "green-screen-creator",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "build": "nuxt build",
7 | "dev": "nuxt dev",
8 | "generate": "nuxt generate",
9 | "preview": "nuxt preview",
10 | "postinstall": "nuxt prepare"
11 | },
12 | "dependencies": {
13 | "@ffmpeg/core": "0.10.0",
14 | "@ffmpeg/ffmpeg": "0.9.8",
15 | "@nuxt/ui": "^2.18.3",
16 | "@vueuse/core": "^10.11.0",
17 | "@vueuse/nuxt": "^10.11.0",
18 | "nuxt": "^3.12.4",
19 | "three": "^0.167.1",
20 | "vue": "latest"
21 | },
22 | "devDependencies": {
23 | "pug": "^3.0.3",
24 | "pug-plain-loader": "^1.1.0",
25 | "stylus": "^0.63.0",
26 | "stylus-loader": "^8.1.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 | div
3 | hero
4 | uploader
5 | demo
6 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'none',
3 | tabWidth: 2,
4 | semi: false,
5 | singleQuote: true,
6 | bracketSpacing: true
7 | }
8 |
--------------------------------------------------------------------------------
/public/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replicate/green-screen-creator/ebbb84cb8bafcd1dee4b2ad1432dde70fe1a4724/public/cover.jpg
--------------------------------------------------------------------------------
/public/cross-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replicate/green-screen-creator/ebbb84cb8bafcd1dee4b2ad1432dde70fe1a4724/public/cross-icon.png
--------------------------------------------------------------------------------
/public/demo/bees.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replicate/green-screen-creator/ebbb84cb8bafcd1dee4b2ad1432dde70fe1a4724/public/demo/bees.mp4
--------------------------------------------------------------------------------
/public/demo/dog.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replicate/green-screen-creator/ebbb84cb8bafcd1dee4b2ad1432dde70fe1a4724/public/demo/dog.mp4
--------------------------------------------------------------------------------
/public/demo/fountain.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replicate/green-screen-creator/ebbb84cb8bafcd1dee4b2ad1432dde70fe1a4724/public/demo/fountain.mp4
--------------------------------------------------------------------------------
/public/demo/jellyfish.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replicate/green-screen-creator/ebbb84cb8bafcd1dee4b2ad1432dde70fe1a4724/public/demo/jellyfish.mp4
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replicate/green-screen-creator/ebbb84cb8bafcd1dee4b2ad1432dde70fe1a4724/public/favicon.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replicate/green-screen-creator/ebbb84cb8bafcd1dee4b2ad1432dde70fe1a4724/public/logo.png
--------------------------------------------------------------------------------
/public/star-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replicate/green-screen-creator/ebbb84cb8bafcd1dee4b2ad1432dde70fe1a4724/public/star-icon.png
--------------------------------------------------------------------------------
/server/api/file.post.js:
--------------------------------------------------------------------------------
1 | export default defineEventHandler(async (event) => {
2 | try {
3 | const { api_token, file_name, data } = await readBody(event)
4 |
5 | // Extract the base64 data from the data URI
6 | const base64Data = data.split(',')[1]
7 |
8 | // Decode base64 to a Uint8Array
9 | const uint8Array = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0))
10 |
11 | // Create a Blob from the Uint8Array
12 | const blob = new Blob([uint8Array], { type: 'application/octet-stream' })
13 |
14 | // Create form data
15 | const form = new FormData()
16 | form.append('content', blob, file_name)
17 |
18 | // Upload the file data to Replicate's file storage
19 | const result = await fetch('https://api.replicate.com/v1/files', {
20 | method: 'POST',
21 | headers: {
22 | Authorization: `Bearer ${api_token}`
23 | },
24 | body: form
25 | })
26 |
27 | const json = await result.json()
28 |
29 | return { data: json.urls.get }
30 | } catch (e) {
31 | console.log('--- error (api/prediction): ', e)
32 |
33 | return {
34 | error: e.message
35 | }
36 | }
37 | })
38 |
--------------------------------------------------------------------------------
/server/api/prediction.get.js:
--------------------------------------------------------------------------------
1 | export default defineEventHandler(async (event) => {
2 | try {
3 | const { api_token, id } = getQuery(event)
4 |
5 | const result = await fetch(
6 | `https://api.replicate.com/v1/predictions/${id}`,
7 | {
8 | method: 'GET',
9 | headers: {
10 | Authorization: `Bearer ${api_token}`
11 | }
12 | }
13 | )
14 |
15 | const json = await result.json()
16 |
17 | // Remove potentially long data
18 | // delete json.logs
19 |
20 | return { data: json }
21 | } catch (e) {
22 | console.log('--- error (api/prediction): ', e)
23 |
24 | return {
25 | error: e.message
26 | }
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/server/api/prediction.post.js:
--------------------------------------------------------------------------------
1 | export default defineEventHandler(async (event) => {
2 | try {
3 | const { api_token, version, input } = await readBody(event)
4 |
5 | const result = await fetch('https://api.replicate.com/v1/predictions', {
6 | method: 'POST',
7 | headers: {
8 | Authorization: `Bearer ${api_token}`
9 | },
10 | body: JSON.stringify({
11 | version,
12 | input
13 | })
14 | })
15 |
16 | const json = await result.json()
17 |
18 | return { data: json }
19 | } catch (e) {
20 | console.log('--- error (api/prediction): ', e)
21 |
22 | return {
23 | error: e.message
24 | }
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../.nuxt/tsconfig.server.json"
3 | }
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://nuxt.com/docs/guide/concepts/typescript
3 | "extends": "./.nuxt/tsconfig.json"
4 | }
5 |
--------------------------------------------------------------------------------