Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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-same-origin 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