Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions maps/function-declarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function embed(location) {
loading="lazy"
allowfullscreen
referrerpolicy="no-referrer-when-downgrade"
sandbox="allow-scripts allow-popups allow-forms allow-popups-to-escape-sandbox"
src="https://www.google.com/maps/embed/v1/place?key=${API_KEY}
&q=${location}"
>
Expand Down
9 changes: 9 additions & 0 deletions maps/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@

<body>
<div id="root"></div>
<div id="controls">
<button id="save-local-button">Save to Local</button>
</div>
<div id="chat-container">
<div id="messages"></div>
<input type="text" id="chat-input" placeholder="Type your message...">
<button id="send-button">Send</button>
<button id="voice-input-button">Voice Input</button>
</div>
<script src="script.js" type="module"></script>
</body>

Expand Down
79 changes: 77 additions & 2 deletions maps/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,20 @@ const chat = async (userText) => {

if (call) {
functionDeclarations[0].callback(call.args);
} else {
const geminiResponse = response.text();
messagesDiv.innerHTML += `<p><b>Gemini:</b> ${geminiResponse}</p>`;
messagesDiv.scrollTop = messagesDiv.scrollHeight;

// Text-to-speech for Gemini's response
const utterance = new SpeechSynthesisUtterance(geminiResponse);
window.speechSynthesis.speak(utterance);
}
} catch (e) {
console.error(e);
}
};
messagesDiv.innerHTML += `<p><b>Gemini:</b> Error: ${e.message}</p>`;
messagesDiv.scrollTop = messagesDiv.scrollHeight;
};

async function init() {
renderPage("%"); // Start by rendering with empty location query: shows earth
Expand All @@ -70,6 +79,72 @@ async function init() {
} else {
document.documentElement.setAttribute("data-theme", "light");
}

const saveLocalButton = document.querySelector("#save-local-button");
saveLocalButton.addEventListener("click", () => {
const location = document.querySelector("#map iframe").src;
const caption = document.querySelector("#caption p")?.textContent || "";
const content = `Location: ${location}\nCaption: ${caption}`;
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "map-data.txt";
a.click();
URL.revokeObjectURL(url);
});

const chatInput = document.querySelector("#chat-input");
const sendButton = document.querySelector("#send-button");
const messagesDiv = document.querySelector("#messages");
const voiceInputButton = document.querySelector("#voice-input-button");

const sendMessage = async () => {
const userText = chatInput.value;
if (userText.trim() === "") return;

messagesDiv.innerHTML += `<p><b>You:</b> ${userText}</p>`;
chatInput.value = "";
await chat(userText);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
};

sendButton.addEventListener("click", sendMessage);
chatInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
sendMessage();
}
});

const handleVoiceInput = () => {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
alert("Speech recognition not supported in this browser.");
return;
}

const recognition = new SpeechRecognition();
recognition.interimResults = false;
recognition.lang = 'en-US';

recognition.addEventListener('result', (e) => {
const transcript = Array.from(e.results)
.map(result => result[0])
.map(result => result.transcript)
.join('');
chatInput.value = transcript;
sendMessage();
});

recognition.addEventListener('error', (e) => {
console.error('Speech recognition error:', e.error);
alert('Speech recognition error: ' + e.error);
});

recognition.start();
};

voiceInputButton.addEventListener("click", handleVoiceInput);
}

init();
Expand Down
14 changes: 13 additions & 1 deletion spatial/src/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

import { useAtom } from "jotai";
import { useResetState } from "./hooks";
import { useResetState, useSaveState } from "./hooks";
import {
DetectTypeAtom,
HoverEnteredAtom,
Expand All @@ -25,6 +25,7 @@ import { modelOptions } from "./consts";

export function TopBar() {
const resetState = useResetState();
const saveState = useSaveState();
const [revealOnHover, setRevealOnHoverMode] = useAtom(RevealOnHoverModeAtom);
const [detectType] = useAtom(DetectTypeAtom);
const [, setHoverEntered] = useAtom(HoverEnteredAtom);
Expand All @@ -45,6 +46,17 @@ export function TopBar() {
>
<div>Reset session</div>
</button>
<button
onClick={() => {
saveState();
}}
className="p-0 border-none underline bg-transparent"
style={{
minHeight: "0",
}}
>
<div>Save to Local</div>
</button>
</div>
<div className="flex gap-3 items-center">
{detectType === "2D bounding boxes" ? (
Expand Down
32 changes: 32 additions & 0 deletions spatial/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
BumpSessionAtom,
ImageSentAtom,
PointsAtom,
ImageSrcAtom,
LinesAtom,
DetectTypeAtom,
} from "./atoms";

export function useResetState() {
Expand All @@ -36,3 +39,32 @@ export function useResetState() {
setPoints([]);
};
}

export function useSaveState() {
const [imageSrc] = useAtom(ImageSrcAtom);
const [boundingBoxes2D] = useAtom(BoundingBoxes2DAtom);
const [boundingBoxes3D] = useAtom(BoundingBoxes3DAtom);
const [points] = useAtom(PointsAtom);
const [lines] = useAtom(LinesAtom);
const [detectType] = useAtom(DetectTypeAtom);

return () => {
const state = {
imageSrc,
boundingBoxes2D,
boundingBoxes3D,
points,
lines,
detectType,
};

const content = JSON.stringify(state, null, 2);
const blob = new Blob([content], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "spatial-data.json";
a.click();
URL.revokeObjectURL(url);
};
}
4 changes: 2 additions & 2 deletions spatial/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ input[type="range"] {
}

.box-caption {
color: var(--bg-color);
background: var(--accent-color);
color: var(--text-color-primary);
background: var(--box-color);
border-radius: var(--box-radius);
padding: 14px 28px;
max-width: 340px;
Expand Down
4 changes: 3 additions & 1 deletion video/server/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import ViteExpress from 'vite-express'
import multer from 'multer'
import {checkProgress, promptVideo, uploadVideo} from './upload.mjs'

import os from 'os';

const app = express()
app.use(express.json())

const upload = multer({dest: '/tmp/'})
const upload = multer({dest: os.tmpdir()})
app.post('/api/upload', upload.single('video'), async (req, res) => {
try {
const file = req.file
Expand Down
28 changes: 28 additions & 0 deletions video/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@ export default function App() {
const isCustomChartMode = isChartMode && chartMode === 'Custom'
const hasSubMode = isCustomMode || isChartMode

const saveState = () => {
const state = {
vidUrl,
timecodeList,
selectedMode,
activeMode,
customPrompt,
chartMode,
chartPrompt,
chartLabel,
};

const content = JSON.stringify(state, null, 2);
const blob = new Blob([content], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "video-data.json";
a.click();
URL.revokeObjectURL(url);
};

const setTimecodes = ({timecodes}) =>
setTimecodeList(
timecodes.map(t => ({...t, text: t.text.replaceAll("\\'", "'")}))
Expand Down Expand Up @@ -238,6 +260,12 @@ export default function App() {
>
▶️ Generate
</button>
<button
className="button saveButton"
onClick={saveState}
>
💾 Save to Local
</button>
</div>
</>
)}
Expand Down
10 changes: 6 additions & 4 deletions video/src/VideoPlayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import {useCallback, useEffect, useState, useMemo} from 'react'
import c from 'classnames'
import {timeToSecs} from './utils'

const formatTime = t =>
`${Math.floor(t / 60)}:${Math.floor(t % 60)
.toString()
.padStart(2, '0')}`
const formatTime = t => {
const minutes = Math.floor(t / 60);
const seconds = Math.floor(t % 60).toString().padStart(2, '0');
const milliseconds = Math.floor((t - Math.floor(t)) * 1000).toString().padStart(3, '0');
return `${minutes}:${seconds}.${milliseconds}`;
}

export default function VideoPlayer({
url,
Expand Down