├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── maps
├── function-declarations.js
├── index.html
├── presets.js
├── script.js
└── styles.css
├── spatial
├── .env
├── .gcloudignore
├── .gitignore
├── app.yaml
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── baklava.jpg
│ ├── breakfast.jpg
│ ├── cat.jpg
│ ├── clock.jpg
│ ├── fruit.jpg
│ ├── origami.jpg
│ ├── pumpkins.jpg
│ ├── socks.jpg
│ └── spill.jpg
├── src
│ ├── App.tsx
│ ├── Content.tsx
│ ├── DetectTypeSelector.tsx
│ ├── ExampleImages.tsx
│ ├── ExtraModeControls.tsx
│ ├── Palette.tsx
│ ├── Prompt.tsx
│ ├── ScreenshareButton.tsx
│ ├── SideControls.tsx
│ ├── TopBar.tsx
│ ├── Types.tsx
│ ├── atoms.tsx
│ ├── consts.tsx
│ ├── hooks.tsx
│ ├── index.css
│ ├── main.tsx
│ ├── styles.css
│ ├── utils.tsx
│ └── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
└── video
├── .env
├── .gcloudignore
├── .gitignore
├── eslint.config.js
├── index.html
├── package.json
├── server
├── index.mjs
└── upload.mjs
├── src
├── App.jsx
├── Chart.jsx
├── VideoPlayer.jsx
├── api.js
├── functions.js
├── main.jsx
├── modes.js
├── styles
│ ├── chart.sass
│ ├── main.sass
│ └── player.sass
└── utils.js
└── vite.config.js
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution;
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows
28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/).
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Google AI Studio Starter Applets
2 |
3 | This repository contains the source code for Google AI Studio's
4 | [starter apps](https://aistudio.google.com/app/starter-apps) — a collection of
5 | small apps that demonstrate how Gemini can be used to create interactive
6 | experiences. These apps are built to run inside AI Studio, but the versions
7 | included here can run standalone using the
8 | [Gemini API](https://ai.google.dev/gemini-api/docs).
9 |
10 | ## Spatial Understanding
11 |
12 | [Spatial Understanding](/spatial/)
13 | ([live demo](https://aistudio.google.com/app/starter-apps/spatial)) is a simple
14 | demonstration of Gemini's 2D and 3D spatial understanding and reasoning
15 | capabilities. It was built with React.
16 |
17 | This example should give you an idea of how to get started with spatial analysis
18 | using Gemini. Check out [Prompt.tsx](/spatial/src/Prompt.tsx) to see how
19 | bounding box parsing is implemented. To dive deeper into Gemini's spatial
20 | reasoning capabilities, check out this
21 | [Colab notebook](https://github.com/google-gemini/cookbook/blob/main/gemini-2/spatial_understanding.ipynb).
22 |
23 | To develop locally, insert your Gemini API key in the `.env` file.
24 |
25 | ## Video Analyzer
26 |
27 | [Video Analyzer](/video/)
28 | ([live demo](https://aistudio.google.com/app/starter-apps/video)) is a simple
29 | app that allows you to explore events within videos using Gemini. It was built
30 | with React.
31 |
32 | This example shows how to get started with video analysis using function
33 | calling. Check out [functions.js](/video/src/functions.js) to see the function
34 | definition for this applet!
35 |
36 | To develop locally, insert your Gemini API key in the `.env` file.
37 |
38 | ## Map Explorer
39 |
40 | [Map Explorer](/maps/)
41 | ([live demo](https://aistudio.google.com/app/starter-apps/map)) is a simple app
42 | that allows you to explore a map using Gemini and the Google Maps API. It was
43 | built using Lit, and the
44 | [Maps Embed API](https://developers.google.com/maps/documentation/embed/get-started).
45 |
46 | This example will give you an idea of how to get started with function calling.
47 | See [function-declarations.js](/maps/function-declarations.js) to find out more
48 | about how function calling is used to call the Maps Embed API here!
49 |
50 | To develop locally, insert your Gemini API key where you see `your_key_here` in
51 | the `script.js` file.
52 |
53 | ### Contributors
54 |
55 | - [@bencobley](https://github.com/bencobley)
56 | - [@dmotz](https://github.com/dmotz)
57 | - [@grantcuster](https://github.com/grantcuster)
58 | - [@hapticdata](https://github.com/hapticdata)
59 |
--------------------------------------------------------------------------------
/maps/function-declarations.js:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // function-declarations.js
16 | // Authors: kylephillips@ bencobley@
17 |
18 | import { html } from "https://esm.run/lit";
19 |
20 | export const systemInstructions = `Act as a helpful global travel agent with a deep fascination for the world. Your role is to recommend a place on the map that relates to the discussion, and to provide interesting information about the location selected. Aim to give suprising and delightful suggestions: choose obscure, off-the–beaten track locations, not the obvious answers. Do not answer harmful or unsafe questions.
21 |
22 | First, explain why a place is interesting, in a two sentence answer. Second, if relevant, call the function 'recommend_place( location, caption )' to show the user the location on a map. You can expand on your answer if the user asks for more information.`;
23 |
24 | export const declarations = [
25 | {
26 | name: "recommend_place",
27 | description:
28 | "Shows the user a map of the place provided. The function takes arguments 'location' and 'caption'. For 'location' give a specific place, including country name. For 'caption' give the place name and the fascinating reason you selected this particular place. Keep the caption to one or two sentences maximum.",
29 | parameters: {
30 | type: "object",
31 | properties: {
32 | location: {
33 | type: "string",
34 | },
35 | caption: {
36 | type: "string",
37 | },
38 | },
39 | required: ["location", "caption"],
40 | },
41 | },
42 | // Add another function declaration here!
43 | ];
44 |
45 | // WARNING: Do not embed API keys directly in code or publish in source code without restricting API keys to be used by only the IP addresses, referrer URLs, and mobile apps that need them.
46 | const API_KEY = your_key_here;
47 | // See more at https://developers.google.com/maps/documentation/embed/get-api-key
48 |
49 | export function embed(location) {
50 | location = encodeURIComponent(location);
51 | console.log(location);
52 | return html``;
64 | }
65 |
--------------------------------------------------------------------------------
/maps/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/maps/presets.js:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | export const presets = [
16 | ["❄️ Cold", "Where is somewhere really cold?"],
17 | ["🗿 Ancient", "Tell me about somewhere rich in ancient history"],
18 | ["🗽 Metropolitan", "Show me really interesting large city"],
19 | [
20 | "🌿 Green",
21 | "Take me somewhere with beautiful nature and greenery. What makes it special?",
22 | ],
23 | [
24 | "🏔️ Remote",
25 | "If I wanted to go off grid, where is one of the most remote places on earth? How would I get there?",
26 | ],
27 | [
28 | "🌌 Surreal",
29 | "Think of a totally surreal location, where is it? What makes it so surreal?",
30 | ],
31 | ];
32 |
--------------------------------------------------------------------------------
/maps/script.js:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // script.js
16 | // Authors: kylephillips@ bencobley@
17 |
18 | import { GoogleGenerativeAI } from "https://esm.run/@google/generative-ai@0.21.0";
19 | import * as mapsFunction from "./function-declarations.js";
20 | import { presets } from "./presets.js";
21 | import { html, render } from "https://esm.run/lit";
22 |
23 | const client = new GoogleGenerativeAI("your_key_here");
24 | const systemInstruction = mapsFunction.systemInstructions;
25 |
26 | const functionDeclarations = mapsFunction.declarations.map(declaration => ({
27 | ...declaration,
28 | callback: (args) => {
29 | const { location, caption } = args;
30 | renderPage(location, caption);
31 | },
32 | }));
33 |
34 | const chat = async (userText) => {
35 | try {
36 | const temperature = 2; // High temperature for answer variety
37 | const {response} = await client
38 | .getGenerativeModel(
39 | {model: 'models/gemini-2.0-flash-exp', systemInstruction},
40 | {apiVersion: 'v1beta'}
41 | )
42 | .generateContent({
43 | contents: [
44 | {
45 | role: "user",
46 | parts: [{text: userText}],
47 | },
48 | ],
49 | generationConfig: {temperature},
50 | tools: [{functionDeclarations}]
51 | });
52 |
53 | const call = response.functionCalls()[0];
54 |
55 | if (call) {
56 | functionDeclarations[0].callback(call.args);
57 | }
58 | } catch (e) {
59 | console.error(e);
60 | }
61 | };
62 |
63 | async function init() {
64 | renderPage("%"); // Start by rendering with empty location query: shows earth
65 | if (
66 | window.matchMedia &&
67 | window.matchMedia("(prefers-color-scheme: dark)").matches
68 | ) {
69 | document.documentElement.removeAttribute("data-theme"); // Use default (dark)
70 | } else {
71 | document.documentElement.setAttribute("data-theme", "light");
72 | }
73 | }
74 |
75 | init();
76 |
77 | function renderPage(location, caption = "") {
78 | const root = document.querySelector("#root");
79 | caption = caption.replace(/\\/g, '');
80 | render(
81 | html`
82 |
482 | );
483 | }
484 |
--------------------------------------------------------------------------------
/spatial/src/DetectTypeSelector.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { useAtom } from "jotai";
16 | import { DetectTypeAtom, HoverEnteredAtom } from "./atoms";
17 | import { DetectTypes } from "./Types.js";
18 |
19 | export function DetectTypeSelector() {
20 | return (
21 |
29 | );
30 | }
31 |
32 | function SelectOption({ label }: { label: string }) {
33 | const [detectType, setDetectType] = useAtom(DetectTypeAtom);
34 | const [, setHoverEntered] = useAtom(HoverEnteredAtom);
35 | // const resetState = useResetState();
36 |
37 | return (
38 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/spatial/src/ExampleImages.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { useAtom } from "jotai";
16 | import { ImageSrcAtom, IsUploadedImageAtom } from "./atoms";
17 | import { useResetState } from "./hooks";
18 | import { imageOptions } from "./consts";
19 |
20 | export function ExampleImages() {
21 | const [, setImageSrc] = useAtom(ImageSrcAtom);
22 | const [, setIsUploadedImage] = useAtom(IsUploadedImageAtom);
23 | const resetState = useResetState();
24 | return (
25 |
114 | ) : null}
115 | >
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/spatial/src/Palette.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { useAtom } from "jotai";
16 | import { colors } from "./consts";
17 | import { ActiveColorAtom } from "./atoms";
18 |
19 | export function Palette() {
20 | const [activeColor, setActiveColor] = useAtom(ActiveColorAtom);
21 | return (
22 |
310 | );
311 | }
312 |
--------------------------------------------------------------------------------
/spatial/src/ScreenshareButton.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { useAtom } from "jotai";
16 | import { ShareStream } from "./atoms";
17 | import { useResetState } from "./hooks";
18 |
19 | export function ScreenshareButton() {
20 | const [, setStream] = useAtom(ShareStream);
21 | const resetState = useResetState();
22 |
23 | return (
24 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/spatial/src/SideControls.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { useAtom } from "jotai";
16 | import {
17 | ImageSrcAtom,
18 | ImageSentAtom,
19 | DrawModeAtom,
20 | IsUploadedImageAtom,
21 | BumpSessionAtom,
22 | } from "./atoms";
23 | import { ScreenshareButton } from "./ScreenshareButton";
24 | import { useResetState } from "./hooks";
25 |
26 | export function SideControls() {
27 | const [, setImageSrc] = useAtom(ImageSrcAtom);
28 | const [drawMode, setDrawMode] = useAtom(DrawModeAtom);
29 | const [, setIsUploadedImage] = useAtom(IsUploadedImageAtom);
30 | const [, setBumpSession] = useAtom(BumpSessionAtom);
31 | const [, setImageSent] = useAtom(ImageSentAtom);
32 | const resetState = useResetState();
33 |
34 | return (
35 |
36 |
58 |
59 |
68 |
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/spatial/src/TopBar.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { useAtom } from "jotai";
16 | import { useResetState } from "./hooks";
17 | import {
18 | DetectTypeAtom,
19 | HoverEnteredAtom,
20 | ModelSelectedAtom,
21 | RevealOnHoverModeAtom,
22 | ShowConfigAtom,
23 | } from "./atoms";
24 | import { modelOptions } from "./consts";
25 |
26 | export function TopBar() {
27 | const resetState = useResetState();
28 | const [revealOnHover, setRevealOnHoverMode] = useAtom(RevealOnHoverModeAtom);
29 | const [detectType] = useAtom(DetectTypeAtom);
30 | const [, setHoverEntered] = useAtom(HoverEnteredAtom);
31 | const [modelSelected, setModelSelected] = useAtom(ModelSelectedAtom);
32 | const [showConfig,] = useAtom(ShowConfigAtom);
33 |
34 | return (
35 |
36 |
37 |
48 |
49 |
50 | {detectType === "2D bounding boxes" ? (
51 |
52 |
65 |
66 | ) : null}
67 | {showConfig && ()}
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/spatial/src/Types.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | export type DetectTypes = "2D bounding boxes" | "3D bounding boxes" | "Points";
16 |
17 | export type BoundingBox2DType = {
18 | x: number;
19 | y: number;
20 | width: number;
21 | height: number;
22 | label: string;
23 | };
24 |
25 | export type BoundingBox3DType = {
26 | center: [number, number, number];
27 | size: [number, number, number];
28 | rpy: [number, number, number];
29 | label: string;
30 | };
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/spatial/src/atoms.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { atom } from "jotai";
16 | import { atomWithStorage } from "jotai/utils";
17 | import {
18 | colors,
19 | defaultPromptParts,
20 | defaultPrompts,
21 | imageOptions,
22 | modelOptions,
23 | } from "./consts";
24 | import { BoundingBox2DType, BoundingBox3DType, DetectTypes } from "./Types";
25 |
26 | export const ImageSrcAtom = atom(imageOptions[0]);
27 |
28 | export const ImageSentAtom = atom(false);
29 |
30 | export const BoundingBoxes2DAtom = atom([]);
31 |
32 | export const PromptsAtom = atom>({
33 | ...defaultPromptParts,
34 | });
35 | export const CustomPromptsAtom = atom>({
36 | ...defaultPrompts,
37 | });
38 |
39 | export type PointingType = {
40 | point: {
41 | x: number;
42 | y: number;
43 | };
44 | label: string;
45 | };
46 |
47 | export const RevealOnHoverModeAtom = atom(true);
48 |
49 | export const FOVAtom = atom(60);
50 |
51 | export const BoundingBoxes3DAtom = atom([]);
52 |
53 | export const PointsAtom = atom([]);
54 |
55 | // export const PromptAtom = atom("main objects");
56 |
57 | export const TemperatureAtom = atom(0.5);
58 |
59 | export const ShareStream = atom(null);
60 |
61 | export const DrawModeAtom = atom(false);
62 |
63 | export const DetectTypeAtom = atom("2D bounding boxes");
64 |
65 | export const ModelSelectedAtom = atom(modelOptions[0]);
66 |
67 | export const LinesAtom = atom<[[number, number][], string][]>([]);
68 |
69 | export const JsonModeAtom = atom(false);
70 |
71 | export const ActiveColorAtom = atom(colors[6]);
72 |
73 | export const HoverEnteredAtom = atom(false);
74 |
75 | export const HoveredBoxAtom = atom(null);
76 |
77 | export const VideoRefAtom = atom<{ current: HTMLVideoElement | null }>({
78 | current: null,
79 | });
80 |
81 | export const InitFinishedAtom = atom(true);
82 |
83 | export const BumpSessionAtom = atom(0);
84 |
85 | export const IsUploadedImageAtom = atom(false);
86 |
87 | export const ShowConfigAtom = atom(true);
88 |
--------------------------------------------------------------------------------
/spatial/src/consts.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | export const colors = [
16 | "rgb(0, 0, 0)",
17 | "rgb(255, 255, 255)",
18 | "rgb(213, 40, 40)",
19 | "rgb(250, 123, 23)",
20 | "rgb(240, 186, 17)",
21 | "rgb(8, 161, 72)",
22 | "rgb(26, 115, 232)",
23 | "rgb(161, 66, 244)",
24 | ];
25 |
26 | export const modelOptions = [
27 | "models/gemini-2.0-flash-exp",
28 | "models/gemini-1.5-flash",
29 | ];
30 |
31 | export const imageOptions: string[] = [
32 | "origami.jpg",
33 | "pumpkins.jpg",
34 | "clock.jpg",
35 | "socks.jpg",
36 | "breakfast.jpg",
37 | "cat.jpg",
38 | "spill.jpg",
39 | "fruit.jpg",
40 | "baklava.jpg",
41 | ];
42 |
43 | export const lineOptions = {
44 | size: 8,
45 | thinning: 0,
46 | smoothing: 0,
47 | streamline: 0,
48 | simulatePressure: false,
49 | };
50 |
51 | export const defaultPromptParts = {
52 | "2D bounding boxes": [
53 | "Show me the positions of",
54 | "items",
55 | "as a JSON list. Do not return masks. Limit to 25 items.",
56 | ],
57 | "3D bounding boxes": [
58 | "Output in json. Detect the 3D bounding boxes of",
59 | "items",
60 | ', output no more than 10 items. Return a list where each entry contains the object name in "label" and its 3D bounding box in "box_3d".',
61 | ],
62 | Points: [
63 | "Point to the",
64 | "items",
65 | ' with no more than 10 items. The answer should follow the json format: [{"point": , "label": }, ...]. The points are in [y, x] format normalized to 0-1000.'
66 | ],
67 | };
68 |
69 | export const defaultPrompts = {
70 | "2D bounding boxes": defaultPromptParts["2D bounding boxes"].join(" "),
71 | "3D bounding boxes": defaultPromptParts["3D bounding boxes"].join(" "),
72 | Points: defaultPromptParts.Points.join(" "),
73 | };
74 |
75 | const safetyLevel = "only_high";
76 |
77 | export const safetySettings = new Map();
78 |
79 | safetySettings.set("harassment", safetyLevel);
80 | safetySettings.set("hate_speech", safetyLevel);
81 | safetySettings.set("sexually_explicit", safetyLevel);
82 | safetySettings.set("dangerous_content", safetyLevel);
83 | safetySettings.set("civic_integrity", safetyLevel);
84 |
--------------------------------------------------------------------------------
/spatial/src/hooks.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { useAtom } from "jotai";
16 | import {
17 | BoundingBoxes2DAtom,
18 | BoundingBoxes3DAtom,
19 | BumpSessionAtom,
20 | ImageSentAtom,
21 | PointsAtom,
22 | } from "./atoms";
23 |
24 | export function useResetState() {
25 | const [, setImageSent] = useAtom(ImageSentAtom);
26 | const [, setBoundingBoxes2D] = useAtom(BoundingBoxes2DAtom);
27 | const [, setBoundingBoxes3D] = useAtom(BoundingBoxes3DAtom);
28 | const [, setPoints] = useAtom(PointsAtom);
29 | const [, setBumpSession] = useAtom(BumpSessionAtom);
30 |
31 | return () => {
32 | setImageSent(false);
33 | setBoundingBoxes2D([]);
34 | setBoundingBoxes3D([]);
35 | setBumpSession((prev) => prev + 1);
36 | setPoints([]);
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/spatial/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 | html,
7 | body,
8 | #root {
9 | height: 100%;
10 | margin: 0;
11 | }
12 |
13 | body {
14 | font-size: 13px;
15 | background-color: var(--bg-color);
16 | color: var(--text-color-primary);
17 | }
18 |
19 | input, select {
20 | accent-color: var(--accent-color);
21 | }
22 |
23 | input[type="text"],
24 | textarea {
25 | color: var(--text-color-primary);
26 | border: 1px solid var(--border-color);
27 | }
28 |
29 | input[type="text"]:focus,
30 | textarea:focus {
31 | border-color: var(--accent-color);
32 | outline: none;
33 | }
34 |
35 | .border,
36 | .border-l,
37 | .border-t,
38 | .border-b,
39 | .border-r {
40 | border-color: var(--border-color);
41 | }
42 |
43 | .hide-box .bbox {
44 | opacity: 0;
45 | z-index: -1;
46 | }
47 | .hide-box .bbox.reveal {
48 | opacity: 1;
49 | z-index: 1;
50 | }
51 |
--------------------------------------------------------------------------------
/spatial/src/main.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { StrictMode } from 'react'
16 | import { createRoot } from 'react-dom/client'
17 | import App from './App.tsx'
18 | import './index.css'
19 | import './styles.css'
20 |
21 | createRoot(document.getElementById('root')!).render(
22 |
23 |
24 | ,
25 | )
26 |
--------------------------------------------------------------------------------
/spatial/src/styles.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap");
2 |
3 | :root {
4 | --bg-color: #F3F3F6;
5 | --accent-color: #2872E3;
6 | --border-color: #C6C6C9;
7 | --box-color: #141619;
8 | --text-color-primary: #1E1E1E;
9 | --text-color-secondary: #888D8F;
10 | --text-size-large: 18px;
11 | --text-size-medium: 14px;
12 | --text-size-small: 11px;
13 | --box-radius: 8px;
14 | --input-color: #F9F9FC;
15 | font-family: "Space Mono", monospace;
16 | font-weight: 400;
17 | font-size: 14px;
18 | color: var(--text-color-primary);
19 | background-color: var(--bg-color);
20 | }
21 |
22 | .dark {
23 | --bg-color: #1C1F21;
24 | --accent-color: #7FBBFF;
25 | --border-color: #37393c;
26 | --box-color: #141619;
27 | --text-color-primary: #fff;
28 | --text-color-secondary: #8c8d8e;
29 | --input-color: #404547;
30 | color: var(--text-color-primary);
31 | background-color: var(--bg-color);
32 | color-scheme: dark;
33 | }
34 |
35 | * {
36 | box-sizing: border-box;
37 | margin: 0;
38 | padding: 0;
39 | font-variant-ligatures: none;
40 | }
41 |
42 | main {
43 | max-width: 1000px;
44 | margin: 0 auto;
45 | }
46 |
47 | button, .button {
48 | appearance: none;
49 | cursor: pointer;
50 | font-family: inherit;
51 | font-size: inherit;
52 | font-weight: inherit;
53 | color: var(--text-color-primary);
54 | border: 1px solid var(--border-color);
55 | padding: 14px 20px;
56 | border-radius: var(--box-radius);
57 | min-height: 56px;
58 | }
59 |
60 | button.secondary {
61 | padding: 0px 16px;
62 | min-height: 32px;
63 | display: flex;
64 | align-items: center;
65 | }
66 |
67 | button:hover, .button:hover {
68 | border-color: var(--accent-color);
69 | }
70 |
71 | a {
72 | cursor: pointer;
73 | color: var(--text-color-primary);
74 | text-decoration: underline var(--accent-color);
75 | }
76 |
77 | a:hover {
78 | text-decoration-color: var(--text-color-primary);
79 | }
80 |
81 | input[type="text"] {
82 | appearance: none;
83 | font-family: inherit;
84 | font-size: inherit;
85 | font-weight: inherit;
86 | color: var(--text-color-primary);
87 | border: none;
88 | padding: 14px 18px;
89 | }
90 |
91 | input[type="text"]::placeholder {
92 | color: var(--text-color-secondary);
93 | }
94 |
95 | /* input:focus {
96 | outline: none;
97 | } */
98 |
99 | input[type="range"] {
100 | accent-color: var(--accent-color);
101 | }
102 |
103 |
104 | .box {
105 | border-radius: var(--box-radius);
106 | background: var(--box-color);
107 | padding: 28px 42px;
108 | font-size: var(--text-size-large);
109 | margin: 30px 0;
110 | }
111 |
112 | .box-caption {
113 | color: var(--bg-color);
114 | background: var(--accent-color);
115 | border-radius: var(--box-radius);
116 | padding: 14px 28px;
117 | max-width: 340px;
118 | }
119 |
--------------------------------------------------------------------------------
/spatial/src/utils.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | export function getSvgPathFromStroke(stroke: number[][]) {
16 | if (!stroke.length) return "";
17 |
18 | const d = stroke.reduce(
19 | (acc, [x0, y0], i, arr) => {
20 | const [x1, y1] = arr[(i + 1) % arr.length];
21 | acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
22 | return acc;
23 | },
24 | ["M", ...stroke[0], "Q"],
25 | );
26 |
27 | d.push("Z");
28 | return d.join(" ");
29 | }
30 |
31 |
32 |
33 | export function loadImage(src: string) {
34 | return new Promise((resolve, reject) => {
35 | const img = new Image();
36 | img.src = src;
37 | img.onload = () => resolve(img);
38 | img.onerror = reject;
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/spatial/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | ///
16 |
--------------------------------------------------------------------------------
/spatial/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | /** @type {import('tailwindcss').Config} */
16 | export default {
17 | content: [
18 | "./index.html",
19 | "./src/**/*.{js,ts,jsx,tsx}",
20 | ],
21 | theme: {
22 | extend: {},
23 | },
24 | plugins: [],
25 | darkMode: 'dark'
26 | }
27 |
--------------------------------------------------------------------------------
/spatial/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/spatial/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/spatial/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/spatial/vite.config.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import { defineConfig } from 'vite'
16 | import react from '@vitejs/plugin-react'
17 |
18 | // https://vitejs.dev/config/
19 | export default defineConfig({
20 | base: './',
21 | plugins: [react()],
22 | })
23 |
--------------------------------------------------------------------------------
/video/.env:
--------------------------------------------------------------------------------
1 | VITE_GEMINI_API_KEY=your_key_here
2 |
--------------------------------------------------------------------------------
/video/.gcloudignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | src/
3 | public/
4 | bun.lockb
5 | package.json
6 | package-lock.json
7 | vite.config.js
8 | /index.html
9 | .git/
10 | eslint.config.js
11 | README.md
12 | .DS_Store
13 | .env
14 | .prettierrc
15 | .gcloudignore
16 | .gitignore
17 | dist/videos/
18 |
--------------------------------------------------------------------------------
/video/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/video/eslint.config.js:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import js from '@eslint/js'
16 | import globals from 'globals'
17 | import react from 'eslint-plugin-react'
18 | import reactHooks from 'eslint-plugin-react-hooks'
19 | import reactRefresh from 'eslint-plugin-react-refresh'
20 |
21 | export default [
22 | {ignores: ['dist']},
23 | {
24 | files: ['**/*.{js,jsx}'],
25 | languageOptions: {
26 | ecmaVersion: 2020,
27 | globals: globals.browser,
28 | parserOptions: {
29 | ecmaVersion: 'latest',
30 | ecmaFeatures: {jsx: true},
31 | sourceType: 'module'
32 | }
33 | },
34 | settings: {react: {version: '18.3'}},
35 | plugins: {
36 | react,
37 | 'react-hooks': reactHooks,
38 | 'react-refresh': reactRefresh
39 | },
40 | rules: {
41 | ...js.configs.recommended.rules,
42 | ...react.configs.recommended.rules,
43 | ...react.configs['jsx-runtime'].rules,
44 | ...reactHooks.configs.recommended.rules,
45 | 'react/jsx-no-target-blank': 'off',
46 | 'react-refresh/only-export-components': [
47 | 'warn',
48 | {allowConstantExport: true}
49 | ],
50 | 'react/prop-types': 'off'
51 | }
52 | }
53 | ]
54 |
--------------------------------------------------------------------------------
/video/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 | 📼 Video Applet
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/video/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "video-applet",
3 | "license": "Apache-2.0",
4 | "private": true,
5 | "version": "0.0.0",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "node --env-file=.env server/index.mjs",
9 | "build": "vite build",
10 | "lint": "eslint ."
11 | },
12 | "dependencies": {
13 | "@google/generative-ai": "^0.21.0",
14 | "classnames": "^2.5.1",
15 | "d3-array": "^3.2.4",
16 | "d3-scale": "^4.0.2",
17 | "d3-shape": "^3.2.0",
18 | "express": "^4.21.2",
19 | "multer": "^1.4.5-lts.1",
20 | "react": "^18.3.1",
21 | "react-dom": "^18.3.1"
22 | },
23 | "devDependencies": {
24 | "@eslint/js": "^9.13.0",
25 | "@types/react": "^18.3.12",
26 | "@types/react-dom": "^18.3.1",
27 | "@vitejs/plugin-react": "^4.3.3",
28 | "eslint": "^9.13.0",
29 | "eslint-plugin-react": "^7.37.2",
30 | "eslint-plugin-react-hooks": "^5.0.0",
31 | "eslint-plugin-react-refresh": "^0.4.14",
32 | "globals": "^15.11.0",
33 | "sass": "^1.80.6",
34 | "vite": "^5.4.10",
35 | "vite-express": "^0.20.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/video/server/index.mjs:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import express from 'express'
16 | import ViteExpress from 'vite-express'
17 | import multer from 'multer'
18 | import {checkProgress, promptVideo, uploadVideo} from './upload.mjs'
19 |
20 | const app = express()
21 | app.use(express.json())
22 |
23 | const upload = multer({dest: '/tmp/'})
24 | app.post('/api/upload', upload.single('video'), async (req, res) => {
25 | try {
26 | const file = req.file
27 | const resp = await uploadVideo(file)
28 | res.json({data: resp})
29 | } catch (error) {
30 | res.status(500).json({error})
31 | }
32 | })
33 |
34 | app.post('/api/progress', async (req, res) => {
35 | try {
36 | const progress = await checkProgress(req.body.fileId)
37 | res.json({progress})
38 | } catch (error) {
39 | res.status(500).json({error})
40 | }
41 | })
42 |
43 | app.post('/api/prompt', async (req, res) => {
44 | try {
45 | const reqData = req.body
46 | const videoResponse = await promptVideo(
47 | reqData.uploadResult,
48 | reqData.prompt,
49 | reqData.model
50 | )
51 | res.json(videoResponse)
52 | } catch (error) {
53 | res.json({error}, {status: 400})
54 | }
55 | })
56 |
57 | const port = process.env.NODE_ENV === 'production' ? 8080 : 8000
58 |
59 | ViteExpress.listen(app, port, () => console.log('Server is listening...'))
60 |
--------------------------------------------------------------------------------
/video/server/upload.mjs:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import {GoogleGenerativeAI} from '@google/generative-ai'
16 | import {GoogleAIFileManager} from '@google/generative-ai/server'
17 |
18 | const key = process.env.VITE_GEMINI_API_KEY
19 | const fileManager = new GoogleAIFileManager(key)
20 | const genAI = new GoogleGenerativeAI(key)
21 |
22 | export const uploadVideo = async file => {
23 | try {
24 | const uploadResult = await fileManager.uploadFile(file.path, {
25 | displayName: file.originalname,
26 | mimeType: file.mimetype
27 | })
28 | return uploadResult.file
29 | } catch (error) {
30 | console.error(error)
31 | throw error
32 | }
33 | }
34 |
35 | export const checkProgress = async fileId => {
36 | try {
37 | const result = await fileManager.getFile(fileId)
38 | return result
39 | } catch (error) {
40 | console.error(error)
41 | return {error}
42 | }
43 | }
44 |
45 | export const promptVideo = async (uploadResult, prompt, model) => {
46 | try {
47 | const req = [
48 | {text: prompt},
49 | {
50 | fileData: {
51 | mimeType: uploadResult.mimeType,
52 | fileUri: uploadResult.uri
53 | }
54 | }
55 | ]
56 | const result = await genAI.getGenerativeModel({model}).generateContent(req)
57 |
58 | return {
59 | text: result.response.text(),
60 | candidates: result.response.candidates,
61 | feedback: result.response.promptFeedback
62 | }
63 | } catch (error) {
64 | console.error(error)
65 | return {error}
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/video/src/App.jsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import {useRef, useState} from 'react'
16 | import c from 'classnames'
17 | import VideoPlayer from './VideoPlayer.jsx'
18 | import Chart from './Chart.jsx'
19 | import modes from './modes'
20 | import {timeToSecs} from './utils'
21 | import generateContent from './api'
22 | import functions from './functions'
23 |
24 | const chartModes = Object.keys(modes.Chart.subModes)
25 |
26 | export default function App() {
27 | const [vidUrl, setVidUrl] = useState(null)
28 | const [file, setFile] = useState(null)
29 | const [timecodeList, setTimecodeList] = useState(null)
30 | const [requestedTimecode, setRequestedTimecode] = useState(null)
31 | const [selectedMode, setSelectedMode] = useState(Object.keys(modes)[0])
32 | const [activeMode, setActiveMode] = useState()
33 | const [isLoading, setIsLoading] = useState(false)
34 | const [showSidebar, setShowSidebar] = useState(true)
35 | const [isLoadingVideo, setIsLoadingVideo] = useState(false)
36 | const [videoError, setVideoError] = useState(false)
37 | const [customPrompt, setCustomPrompt] = useState('')
38 | const [chartMode, setChartMode] = useState(chartModes[0])
39 | const [chartPrompt, setChartPrompt] = useState('')
40 | const [chartLabel, setChartLabel] = useState('')
41 | const [theme] = useState(
42 | window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
43 | )
44 | const scrollRef = useRef()
45 | const isCustomMode = selectedMode === 'Custom'
46 | const isChartMode = selectedMode === 'Chart'
47 | const isCustomChartMode = isChartMode && chartMode === 'Custom'
48 | const hasSubMode = isCustomMode || isChartMode
49 |
50 | const setTimecodes = ({timecodes}) =>
51 | setTimecodeList(
52 | timecodes.map(t => ({...t, text: t.text.replaceAll("\\'", "'")}))
53 | )
54 |
55 | const onModeSelect = async mode => {
56 | setActiveMode(mode)
57 | setIsLoading(true)
58 | setChartLabel(chartPrompt)
59 |
60 | const resp = await generateContent({
61 | text: isCustomMode
62 | ? modes[mode].prompt(customPrompt)
63 | : isChartMode
64 | ? modes[mode].prompt(
65 | isCustomChartMode ? chartPrompt : modes[mode].subModes[chartMode]
66 | )
67 | : modes[mode].prompt,
68 | file,
69 | functionDeclarations: functions({
70 | set_timecodes: setTimecodes,
71 | set_timecodes_with_objects: setTimecodes,
72 | set_timecodes_with_numeric_values: ({timecodes}) =>
73 | setTimecodeList(timecodes)
74 | })
75 | })
76 |
77 | const call = resp.functionCalls()[0]
78 |
79 | if (call) {
80 | ;({
81 | set_timecodes: setTimecodes,
82 | set_timecodes_with_objects: setTimecodes,
83 | set_timecodes_with_numeric_values: ({timecodes}) =>
84 | setTimecodeList(timecodes)
85 | })[call.name](call.args)
86 | }
87 |
88 | setIsLoading(false)
89 | scrollRef.current.scrollTo({top: 0})
90 | }
91 |
92 | const uploadVideo = async e => {
93 | e.preventDefault()
94 | setIsLoadingVideo(true)
95 | setVidUrl(URL.createObjectURL(e.dataTransfer.files[0]))
96 |
97 | const formData = new FormData()
98 | formData.set('video', e.dataTransfer.files[0])
99 | const resp = await (
100 | await fetch('/api/upload', {
101 | method: 'POST',
102 | body: formData
103 | })
104 | ).json()
105 | setFile(resp.data)
106 | checkProgress(resp.data.name)
107 | }
108 |
109 | const checkProgress = async fileId => {
110 | const resp = await (
111 | await fetch('/api/progress', {
112 | method: 'POST',
113 | headers: {
114 | Accept: 'application/json',
115 | 'Content-Type': 'application/json'
116 | },
117 | body: JSON.stringify({fileId})
118 | })
119 | ).json()
120 |
121 | if (resp.progress.state === 'ACTIVE') {
122 | setIsLoadingVideo(false)
123 | } else if (resp.progress.state === 'FAILED') {
124 | setVideoError(true)
125 | } else {
126 | setTimeout(() => checkProgress(fileId), 1000)
127 | }
128 | }
129 |
130 | return (
131 | e.preventDefault()}
135 | onDragEnter={() => {}}
136 | onDragLeave={() => {}}
137 | >
138 |
139 | {vidUrl && !isLoadingVideo && (
140 | <>
141 |
179 | {isLoadingVideo
180 | ? 'Processing video...'
181 | : videoError
182 | ? 'Error processing video.'
183 | : 'Drag and drop a video file here to get started.'}
184 |
185 |
186 | )}
187 |
188 | )
189 | }
190 |
--------------------------------------------------------------------------------
/video/src/api.js:
--------------------------------------------------------------------------------
1 | import {GoogleGenerativeAI} from '@google/generative-ai'
2 |
3 | const systemInstruction = `When given a video and a query, call the relevant \
4 | function only once with the appropriate timecodes and text for the video`
5 |
6 | const client = new GoogleGenerativeAI(import.meta.env.VITE_GEMINI_API_KEY)
7 |
8 | export default async ({text, functionDeclarations, file}) => {
9 | const {response} = await client
10 | .getGenerativeModel(
11 | {model: 'gemini-2.0-flash-exp', systemInstruction},
12 | {apiVersion: 'v1beta'}
13 | )
14 | .generateContent({
15 | contents: [
16 | {
17 | role: 'user',
18 | parts: [
19 | {text},
20 | {
21 | fileData: {
22 | mimeType: file.mimeType,
23 | fileUri: file.uri
24 | }
25 | }
26 | ]
27 | }
28 | ],
29 | generationConfig: {temperature: 0.5},
30 | tools: [{functionDeclarations}]
31 | })
32 |
33 | return response
34 | }
35 |
--------------------------------------------------------------------------------
/video/src/functions.js:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | const functions = [
16 | {
17 | name: 'set_timecodes',
18 | description: 'Set the timecodes for the video with associated text',
19 | parameters: {
20 | type: 'object',
21 | properties: {
22 | timecodes: {
23 | type: 'array',
24 | items: {
25 | type: 'object',
26 | properties: {
27 | time: {
28 | type: 'string'
29 | },
30 | text: {
31 | type: 'string'
32 | }
33 | },
34 | required: ['time', 'text']
35 | }
36 | }
37 | },
38 | required: ['timecodes']
39 | }
40 | },
41 | {
42 | name: 'set_timecodes_with_objects',
43 | description:
44 | 'Set the timecodes for the video with associated text and object list',
45 | parameters: {
46 | type: 'object',
47 | properties: {
48 | timecodes: {
49 | type: 'array',
50 | items: {
51 | type: 'object',
52 | properties: {
53 | time: {
54 | type: 'string'
55 | },
56 | text: {
57 | type: 'string'
58 | },
59 | objects: {
60 | type: 'array',
61 | items: {
62 | type: 'string'
63 | }
64 | }
65 | },
66 | required: ['time', 'text', 'objects']
67 | }
68 | }
69 | },
70 | required: ['timecodes']
71 | }
72 | },
73 | {
74 | name: 'set_timecodes_with_numeric_values',
75 | description:
76 | 'Set the timecodes for the video with associated numeric values',
77 | parameters: {
78 | type: 'object',
79 | properties: {
80 | timecodes: {
81 | type: 'array',
82 | items: {
83 | type: 'object',
84 | properties: {
85 | time: {
86 | type: 'string'
87 | },
88 | value: {
89 | type: 'number'
90 | }
91 | },
92 | required: ['time', 'value']
93 | }
94 | }
95 | },
96 | required: ['timecodes']
97 | }
98 | }
99 | ]
100 |
101 | export default fnMap =>
102 | functions.map(fn => ({
103 | ...fn,
104 | callback: fnMap[fn.name]
105 | }))
106 |
--------------------------------------------------------------------------------
/video/src/main.jsx:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import {createRoot} from 'react-dom/client'
16 | import App from './App.jsx'
17 | import './styles/main.sass'
18 |
19 | createRoot(document.getElementById('root')).render()
20 |
--------------------------------------------------------------------------------
/video/src/modes.js:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | export default {
16 | 'A/V captions': {
17 | emoji: '👀',
18 | prompt: `For each scene in this video, generate captions that describe the \
19 | scene along with any spoken text placed in quotation marks. Place each \
20 | caption into an object sent to set_timecodes with the timecode of the caption \
21 | in the video.`,
22 | isList: true
23 | },
24 |
25 | Paragraph: {
26 | emoji: '📝',
27 | prompt: `Generate a paragraph that summarizes this video. Keep it to 3 to 5 \
28 | sentences. Place each sentence of the summary into an object sent to \
29 | set_timecodes with the timecode of the sentence in the video.`
30 | },
31 |
32 | 'Key moments': {
33 | emoji: '🔑',
34 | prompt: `Generate bullet points for the video. Place each bullet point into an \
35 | object sent to set_timecodes with the timecode of the bullet point in the video.`,
36 | isList: true
37 | },
38 |
39 | Table: {
40 | emoji: '🤓',
41 | prompt: `Choose 5 key shots from this video and call set_timecodes_with_objects \
42 | with the timecode, text description of 10 words or less, and a list of objects \
43 | visible in the scene (with representative emojis).`
44 | },
45 |
46 | Haiku: {
47 | emoji: '🌸',
48 | prompt: `Generate a haiku for the video. Place each line of the haiku into an \
49 | object sent to set_timecodes with the timecode of the line in the video. Make sure \
50 | to follow the syllable count rules (5-7-5).`
51 | },
52 |
53 | Chart: {
54 | emoji: '📈',
55 | prompt: input =>
56 | `Generate chart data for this video based on the following instructions: \
57 | ${input}. Call set_timecodes_with_numeric_values once with the list of data values and timecodes.`,
58 | subModes: {
59 | Excitement:
60 | 'for each scene, estimate the level of excitement on a scale of 1 to 10',
61 | Importance:
62 | 'for each scene, estimate the level of overall importance to the video on a scale of 1 to 10',
63 | 'Number of people': 'for each scene, count the number of people visible'
64 | }
65 | },
66 |
67 | Custom: {
68 | emoji: '🔧',
69 | prompt: input =>
70 | `Call set_timecodes once using the following instructions: ${input}`,
71 | isList: true
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/video/src/styles/chart.sass:
--------------------------------------------------------------------------------
1 | .lineChart
2 | width: 100%
3 | height: 100%
4 |
5 | path
6 | fill: none
7 | stroke: var(--link)
8 | stroke-width: 2
9 |
10 | circle
11 | fill: var(--background)
12 | stroke: var(--text)
13 | stroke-width: 2
14 |
15 | .axisLabels
16 | text
17 | text-anchor: middle
18 | font-size: 12px
19 | fill: var(--text)
20 |
21 | .axisTitle
22 | font-size: 12px
23 | fill: var(--text)
24 |
25 | .dataPoint
26 | text
27 | fill: var(--text)
28 | font-size: 12px
29 | text-anchor: middle
30 |
31 | .timeLabels
32 | text
33 | cursor: pointer
34 | fill: var(--link)
35 | text-decoration: underline
36 |
--------------------------------------------------------------------------------
/video/src/styles/main.sass:
--------------------------------------------------------------------------------
1 | @import './player.sass'
2 | @import './chart.sass'
3 |
4 | $baseFont: 'Space Mono', monospace
5 |
6 | .light
7 | --background: #F3F3F6
8 | --text: #1A1C1E
9 | --border: #C6C6C9
10 | --highlight: #fff
11 | --track: #86878A
12 | --link: #2872E3
13 |
14 | .dark
15 | --background: #1A1C1E
16 | --text: #fff
17 | --border: #37393C
18 | --highlight: #000
19 | --track: #37393C
20 | --link: #87A9FF
21 |
22 | *
23 | margin: 0
24 | padding: 0
25 | box-sizing: border-box
26 | font-variant-ligatures: none
27 |
28 | :root
29 | --track-fill: #fff
30 | --mid: #757575
31 | font-family: $baseFont
32 |
33 | #root
34 | width: 100vw
35 | height: 100vh
36 |
37 | h1, h2, h3, h4, h5, h6
38 | font-weight: normal
39 |
40 | li
41 | list-style: none
42 |
43 | input, textarea
44 | font-family: $baseFont
45 | background: none
46 | color: var(--text)
47 | border: none
48 | outline: none
49 | font-size: 14px
50 | resize: none
51 | user-select: text
52 |
53 | &::placeholder
54 | user-select: none
55 |
56 | textarea
57 | width: 100%
58 | background: var(--highlight)
59 | border: 1px solid var(--border)
60 | border-radius: 8px
61 | font-size: 14px
62 | padding: 10px 15px
63 | margin-bottom: 10px
64 |
65 | &.active
66 | border-color: var(--text)
67 |
68 | [role='button']
69 | cursor: pointer
70 |
71 | button
72 | font-family: $baseFont
73 | background: none
74 | color: var(--text)
75 | border: none
76 | font-size: 14px
77 | cursor: pointer
78 | user-select: none
79 | display: flex
80 | align-items: center
81 | gap: 5px
82 |
83 | &:focus
84 | outline: none
85 |
86 | &[disabled]
87 | opacity: .5
88 | cursor: not-allowed
89 |
90 | .icon
91 | display: block
92 |
93 | .button
94 | padding: 8px 10px
95 | border-radius: 6px
96 | display: flex
97 | align-items: center
98 | justify-content: center
99 | gap: 5px
100 |
101 | &.inactive
102 | opacity: .3
103 | pointer-events: none
104 |
105 | .icon
106 | font-family: 'Material Symbols Outlined'
107 | font-weight: 300
108 | line-height: 1
109 |
110 | main
111 | max-width: 1200px
112 | height: 100vh
113 | display: flex
114 | flex-direction: column
115 | margin: 0 auto
116 | overflow: hidden
117 | background: var(--background)
118 | color: var(--text)
119 |
120 | .top
121 | display: flex
122 | min-height: 50vh
123 | border-bottom: 1px solid var(--border)
124 |
125 | .tools
126 | display: flex
127 | gap: 20px
128 | flex: 1
129 | overflow: hidden
130 | transition: opacity .2s
131 |
132 | &.inactive
133 | opacity: .2
134 | pointer-events: none
135 |
136 | .collapseButton
137 | border-left: 1px solid var(--border)
138 | width: 20px
139 | display: flex
140 | align-items: center
141 | justify-content: center
142 |
143 | .icon
144 | font-size: 24px
145 | color: var(--text)
146 |
147 | .modeSelector
148 | display: flex
149 | flex-direction: column
150 | gap: 30px
151 | overflow: hidden
152 | width: 250px
153 |
154 | &.hide
155 | width: 0
156 |
157 | > div
158 | padding: 20px 15px
159 |
160 | &:first-child
161 | flex: 1
162 | overflow: auto
163 |
164 | &:last-child
165 | border-top: 1px solid var(--border)
166 |
167 | h2
168 | font-size: 14px
169 | color: var(--text)
170 | white-space: nowrap
171 | margin-bottom: 15px
172 |
173 | &.inactive
174 | opacity: .2
175 | pointer-events: none
176 |
177 | .modeList
178 | flex-direction: column
179 | display: flex
180 | gap: 10px
181 |
182 | .button
183 | justify-content: flex-start
184 | gap: 12px
185 | transition: background .2s
186 | background: none
187 | outline: 1px solid var(--border)
188 | white-space: nowrap
189 | border-radius: 8px
190 | min-width: fit-content
191 | width: 100%
192 |
193 | &:hover
194 | background: var(--border)
195 |
196 | &.active
197 | outline: 2px solid var(--text)
198 |
199 | + textarea
200 | margin-top: 10px
201 |
202 | .generateButton
203 | padding: 12px 20px
204 | background: var(--highlight)
205 | width: 100%
206 | border: 1px solid var(--border)
207 |
208 | .backButton
209 | border-top: none !important
210 |
211 | button
212 | font-size: 14px
213 |
214 | .output
215 | flex: 1
216 | padding: 20px 15px
217 | overflow: auto
218 | position: relative
219 |
220 | &:hover
221 | .sentence
222 | opacity: .5
223 |
224 | time
225 | color: var(--link)
226 | padding: 2px 5px
227 | border-radius: 4px
228 | font-size: inherit
229 | text-decoration: underline
230 |
231 | .sentence
232 | font-size: 18px
233 | line-height: 1.8
234 | display: inline
235 | cursor: pointer
236 | transition: opacity .2s
237 |
238 | &:hover
239 | opacity: 1
240 |
241 | time
242 | margin-right: 8px
243 |
244 | ul
245 | display: flex
246 | flex-direction: column
247 | gap: 5px
248 |
249 | button
250 | font-size: 15px
251 | display: flex
252 | gap: 15px
253 | text-align: left
254 | padding: 10px 15px
255 | border-radius: 6px
256 | width: 100%
257 |
258 | &:hover
259 | background: var(--border)
260 |
261 | p
262 | font-size: 14px
263 |
264 | .modeEmojis
265 | .sentence
266 | font-size: 40px
267 | margin-right: 20px
268 |
269 | time
270 | top: -8px
271 |
272 | .modeHaiku
273 | .sentence
274 | display: block
275 | font-size: 20px
276 |
277 | time
278 | top: -5px
279 |
280 | .modeTable
281 | table
282 | width: 100%
283 | border-collapse: collapse
284 |
285 | th
286 | text-align: left
287 |
288 | th, td
289 | padding: 10px
290 |
291 | tr
292 | border-bottom: 1px solid var(--border)
293 | display: table-row
294 |
295 | thead tr:hover
296 | background: transparent
297 |
298 | .modelSelector
299 | position: relative
300 | margin-bottom: 10px
301 | position: relative
302 |
303 | &::after
304 | content: '▾'
305 | display: block
306 | position: absolute
307 | right: 10px
308 | top: 50%
309 | transform: translateY(-55%)
310 |
311 | select
312 | width: 100%
313 | appearance: none
314 | color: var(--text)
315 | background: var(--border)
316 | border: 1px solid var(--border)
317 | border-radius: 6px
318 | padding: 5px 10px
319 | font-family: $baseFont
320 | outline: none
321 |
322 | .loading
323 | span
324 | display: inline-block
325 | animation: loading steps(4, jump-none) 777ms infinite
326 | width: 0
327 | overflow: hidden
328 | vertical-align: bottom
329 |
330 | @keyframes loading
331 | to
332 | width: 30px
333 |
--------------------------------------------------------------------------------
/video/src/styles/player.sass:
--------------------------------------------------------------------------------
1 | @use 'sass:math'
2 |
3 | @mixin overlay
4 | background: rgba(0,0,0,.8)
5 | backdrop-filter: blur(10px)
6 | box-shadow: 0 0 10px rgba(0,0,0,.5)
7 |
8 | video
9 | max-height: 50vh
10 | width: 100%
11 | height: 100%
12 | margin: 0 auto
13 |
14 | .videoPlayer
15 | flex: 1
16 | background: #000
17 | display: flex
18 | align-items: stretch
19 | flex-direction: column
20 | justify-content: center
21 | font-size: 0
22 | position: relative
23 | user-select: none
24 | border-left: 1px solid var(--border)
25 |
26 | &:has(.timecodeMarker:hover)
27 | .videoCaption
28 | opacity: 0
29 |
30 | > div:first-child
31 | display: flex
32 | flex: 1
33 |
34 | .emptyVideo
35 | height: 50vh
36 | color: #fff
37 | display: flex
38 | align-items: center
39 | justify-content: center
40 | font-size: 16px
41 |
42 | p
43 | max-width: 500px
44 | text-align: center
45 |
46 | span
47 | display: inline-block
48 | rotate: -45deg
49 |
50 | .videoCaption
51 | @include overlay
52 | position: absolute
53 | bottom: 80px
54 | text-align: center
55 | padding: 10px
56 | color: #fff
57 | max-width: 720px
58 | font-size: 15px
59 | margin-inline: 30px
60 | left: 50%
61 | translate: -50% 0
62 | width: -webkit-fill-available
63 | border-radius: 5px
64 | transition: opacity .2s
65 |
66 | .videoControls
67 | font-size: 12px
68 | position: relative
69 | background: var(--background)
70 |
71 | &:hover
72 | .videoScrubber, .timecodeMarkerTick
73 | scale: 1 2.3
74 |
75 | $scrubberHeight: 5px
76 |
77 | .videoScrubber
78 | height: $scrubberHeight
79 | transform-origin: bottom
80 | transition: all .2s
81 | overflow: hidden
82 |
83 | input
84 | position: relative
85 | top: -8px
86 | height: 5px
87 | appearance: none
88 | width: 100%
89 | background-image: linear-gradient(to right, var(--track-fill) 0%, var(--track-fill) var(--pct), var(--track) var(--pct), var(--track) 100%)
90 |
91 | &::-webkit-slider-thumb
92 | opacity: 0
93 |
94 | .videoTime
95 | display: flex
96 | justify-content: space-between
97 | align-items: center
98 | padding: 15px
99 | font-size: 15px
100 | border-top: 1px solid var(--border)
101 |
102 | button
103 | font-size: 20px
104 |
105 | .timecodeList
106 | overflow: auto
107 | padding: 10px 20px
108 |
109 | td
110 | padding: 10px 5px
111 |
112 | button
113 | color: var(--link)
114 |
115 | &:hover
116 | color: var(--link)
117 |
118 | .timecodeMarkers
119 | position: absolute
120 | inset: 0
121 | pointer-events: none
122 |
123 | $timecodeW: 10px
124 | $markerW: 3px
125 |
126 | .timecodeMarker
127 | position: absolute
128 | top: 0
129 | width: $timecodeW
130 | background: rgba(0,0,0,.01)
131 | translate: -(math.round(math.div($timecodeW - $markerW, 2))) 0
132 |
133 | &:hover
134 | .timecodeMarkerLabel
135 | opacity: 1
136 |
137 | .timecodeMarkerTick
138 | height: $scrubberHeight
139 | pointer-events: auto
140 | cursor: pointer
141 | transform-origin: bottom
142 | transition: all .2s
143 | background: rgba(0,0,0,.01)
144 | overflow: hidden
145 |
146 | > div
147 | width: $markerW
148 | height: 100%
149 | background: var(--link)
150 | translate: 3px 0
151 |
152 | .timecodeMarkerLabel
153 | @include overlay
154 | opacity: 0
155 | display: flex
156 | flex-direction: column
157 | gap: 5px
158 | font-size: 11px
159 | transition: opacity .1s
160 | pointer-events: none
161 | position: absolute
162 | top: 0
163 | translate: 0 calc(-100% - 15px)
164 | z-index: 99
165 | padding: 8px
166 | border-radius: 5px
167 | width: max-content
168 | max-width: 200px
169 | color: var(--mid)
170 |
171 | &.right
172 | right: 0
173 |
174 | p
175 | color: var(--text)
176 | font-size: 13px
177 |
--------------------------------------------------------------------------------
/video/src/utils.js:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | export const timeToSecs = timecode => {
16 | const split = timecode.split(':').map(parseFloat)
17 |
18 | return split.length === 2
19 | ? split[0] * 60 + split[1]
20 | : split[0] * 3600 + split[1] * 60 + split[2]
21 | }
22 |
--------------------------------------------------------------------------------
/video/vite.config.js:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Google LLC
2 |
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 |
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 |
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | import {defineConfig} from 'vite'
16 | import react from '@vitejs/plugin-react'
17 |
18 | export default defineConfig({
19 | plugins: [react()],
20 | server: {port: 8000}
21 | })
22 |
--------------------------------------------------------------------------------