Vanilla JavaScript
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
const videoElement = document.getElementById('video');
const statusElement = document.getElementById('status');
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
async function connect() {
try {
await client.connect({
onConnected: (mediaStream) => {
videoElement.srcObject = mediaStream;
videoElement.play();
},
onDisconnected: () => {
videoElement.srcObject = null;
},
onStatusChange: (status, message) => {
statusElement.textContent = message || status;
},
onError: (error, fatal) => {
console.error('Error:', error.message);
if (fatal) {
statusElement.textContent = 'Connection failed: ' + error.message;
}
},
});
} catch (error) {
console.error('Failed to connect:', error.message);
statusElement.textContent = 'Connection failed: ' + error.message;
}
}
async function startStream() {
const streamId = await client.startStream({ prompt: 'A cat', portrait: true });
console.log('Stream started:', streamId);
}
async function interact(prompt) {
const ack = await client.interact({ prompt });
console.log('Acknowledged:', ack);
}
function disconnect() {
client.disconnect();
}
// Usage
connect();
document.getElementById('start-btn').onclick = startStream;
document.getElementById('interact-btn').onclick = () => interact('Pet the cat');
document.getElementById('disconnect-btn').onclick = disconnect;
HTML Boilerplate
Copy
Ask AI
<!DOCTYPE html>
<html>
<head>
<title>Odyssey Demo</title>
</head>
<body>
<video id="video" autoplay playsinline muted></video>
<p id="status">Disconnected</p>
<button id="start-btn">Start Stream</button>
<button id="interact-btn">Interact</button>
<button id="disconnect-btn">Disconnect</button>
<script type="module">
import { Odyssey } from '@odysseyml/odyssey';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
const video = document.getElementById('video');
const status = document.getElementById('status');
try {
await client.connect({
onConnected: (mediaStream) => {
video.srcObject = mediaStream;
status.textContent = 'Connected';
},
onError: (error, fatal) => {
console.error('Error:', error.message);
if (fatal) status.textContent = 'Connection failed';
},
onStatusChange: (s, msg) => {
status.textContent = msg || s;
},
});
document.getElementById('start-btn').onclick = async () => {
await client.startStream({ prompt: 'A cat' });
};
} catch (error) {
console.error('Failed to connect:', error.message);
status.textContent = 'Connection failed';
}
document.getElementById('interact-btn').onclick = async () => {
await client.interact({ prompt: 'Pet the cat' });
};
document.getElementById('disconnect-btn').onclick = () => {
client.disconnect();
};
</script>
</body>
</html>
React with TypeScript
Copy
Ask AI
import { useEffect, useRef, useState } from 'react';
import { useOdyssey } from '@odysseyml/odyssey/react';
function App() {
const videoRef = useRef<HTMLVideoElement>(null);
const [prompt, setPrompt] = useState('');
const odyssey = useOdyssey({
apiKey: 'ody_your_api_key_here',
handlers: {
onConnected: (mediaStream) => {
if (videoRef.current) {
videoRef.current.srcObject = mediaStream;
videoRef.current.play();
}
},
onDisconnected: () => {
if (videoRef.current) {
videoRef.current.srcObject = null;
}
},
onError: (error, fatal) => {
console.error('Error:', error.message, 'Fatal:', fatal);
},
onStreamStarted: (streamId) => {
console.log('Stream started:', streamId);
},
onInteractAcknowledged: (ackPrompt) => {
console.log('Interaction acknowledged:', ackPrompt);
},
onStreamError: (reason, message) => {
console.error('Stream error:', reason, message);
},
},
});
useEffect(() => {
odyssey.connect()
.then(stream => console.log('Connected with stream:', stream.id))
.catch(err => console.error('Connection failed:', err.message));
return () => odyssey.disconnect();
}, []);
const handleStart = async () => {
await odyssey.startStream({ prompt: 'A cat at sunset', portrait: true });
};
const handleInteract = async () => {
if (prompt.trim()) {
await odyssey.interact({ prompt });
setPrompt('');
}
};
const handleEnd = async () => {
await odyssey.endStream();
};
return (
<div>
<video ref={videoRef} autoPlay playsInline muted />
<div>
<p>Status: {odyssey.status}</p>
{odyssey.error && <p style={{ color: 'red' }}>{odyssey.error}</p>}
</div>
<div>
<button onClick={handleStart} disabled={!odyssey.isConnected}>
Start Stream
</button>
<input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Enter interaction prompt..."
/>
<button onClick={handleInteract} disabled={!odyssey.isConnected}>
Send
</button>
<button onClick={handleEnd} disabled={!odyssey.isConnected}>
End Stream
</button>
</div>
</div>
);
}
export default App;
Error Handling Pattern
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
await client.connect({
onError: (error, fatal) => {
if (fatal) {
// Connection cannot continue - show error UI
showErrorPage(error.message);
} else {
// Recoverable - show notification
showNotification(`Warning: ${error.message}`);
}
},
onStreamError: (reason, message) => {
// Stream-specific error (e.g., model crashed)
console.error(`Stream error [${reason}]: ${message}`);
// You might want to restart the stream
await client.endStream();
await client.startStream({ prompt: 'Recovery prompt' });
},
});
Status Monitoring
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
await client.connect({
onStatusChange: (status, message) => {
switch (status) {
case 'authenticating':
showLoader('Authenticating...');
break;
case 'connecting':
showLoader('Connecting to server...');
break;
case 'reconnecting':
showLoader('Reconnecting...');
break;
case 'connected':
hideLoader();
showSuccess('Connected!');
break;
case 'disconnected':
showInfo('Disconnected');
break;
case 'failed':
showError(message || 'Connection failed');
break;
}
},
});
Image-to-Video
Image-to-video requirements:
- SDK version 1.0.0+
- Max size: 25MB
- Supported formats: JPEG, PNG, WebP, GIF, BMP, HEIC, HEIF, AVIF
- Images are resized to 1280x704 (landscape) or 704x1280 (portrait)
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
// File input handler
const fileInput = document.getElementById('image-input') as HTMLInputElement;
fileInput.addEventListener('change', async () => {
const imageFile = fileInput.files?.[0];
if (!imageFile) return;
// Connect to Odyssey
const mediaStream = await client.connect();
document.querySelector('video').srcObject = mediaStream;
// Start stream with the image
const streamId = await client.startStream({
prompt: 'A cat',
portrait: false, // landscape
image: imageFile
});
console.log('Stream started:', streamId);
// Interact as usual
await client.interact({ prompt: 'Pet the cat' });
});
// Cleanup
window.addEventListener('beforeunload', () => client.disconnect());
Working with the Simulate API
The Simulate API requires SDK version 1.0.0 or higher.
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
// Create a simulation with a scripted sequence
const job = await client.simulate({
script: [
{ timestamp_ms: 0, start: { prompt: 'A cat sitting on a windowsill' } },
{ timestamp_ms: 3000, interact: { prompt: 'The cat watches a bird outside' } },
{ timestamp_ms: 6000, interact: { prompt: 'The cat stretches lazily' } },
{ timestamp_ms: 9000, end: {} }
],
portrait: true
});
console.log('Simulation started:', job.job_id);
// Poll for completion
async function waitForCompletion(jobId: string) {
while (true) {
const status = await client.getSimulateStatus(jobId);
if (status.status === 'completed') {
return status;
}
if (status.status === 'failed') {
throw new Error(`Simulation failed: ${status.error_message}`);
}
if (status.status === 'cancelled') {
throw new Error('Simulation was cancelled');
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
const result = await waitForCompletion(job.job_id);
// Get recordings from completed simulation
for (const stream of result.streams) {
const recording = await client.getRecording(stream.stream_id);
console.log('Video URL:', recording.video_url);
}
Example Application: Slack GIF Bot
This example shows a Slack bot that generates GIFs using the Simulate API. Users can mention the bot with a prompt like@ody gif A cat sleeping and it will generate and upload a GIF.
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
const odyssey = new Odyssey({ apiKey: 'ody_your_api_key_here' });
/**
* Generate a video from a prompt using the Simulate API
*/
async function generateVideo(
prompt: string,
durationSeconds: number,
image?: Buffer
): Promise<string> {
// Build the start entry with optional image for image-to-video
const startEntry: { prompt: string; image?: string } = { prompt };
if (image) {
startEntry.image = `data:image/png;base64,${image.toString('base64')}`;
}
// Submit simulation job
const job = await odyssey.simulate({
script: [
{ timestamp_ms: 0, start: startEntry },
{ timestamp_ms: durationSeconds * 1000, end: {} },
],
portrait: false,
});
console.log(`Simulation started: ${job.job_id}`);
// Poll for completion
const videoUrl = await waitForSimulation(job.job_id);
return videoUrl;
}
/**
* Poll for simulation completion
*/
async function waitForSimulation(jobId: string): Promise<string> {
const maxWaitMs = 5 * 60 * 1000; // 5 minutes
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const status = await odyssey.getSimulateStatus(jobId);
if (status.status === 'completed') {
const videoUrl = status.streams[0]?.video_url;
if (!videoUrl) {
throw new Error('Simulation completed but no video URL available');
}
return videoUrl;
}
if (status.status === 'failed') {
throw new Error(`Simulation failed: ${status.error_message}`);
}
if (status.status === 'cancelled') {
throw new Error('Simulation was cancelled');
}
// Wait 2 seconds before polling again
await new Promise((resolve) => setTimeout(resolve, 2000));
}
throw new Error('Simulation timed out');
}
// Usage with Slack Bolt
app.event('app_mention', async ({ event, client }) => {
const prompt = event.text.replace(/<@[A-Z0-9]+>/gi, '').trim();
try {
// Generate video
const videoUrl = await generateVideo(prompt, 5);
// Download and convert to GIF, then upload to Slack
// (GIF conversion code omitted for brevity)
await client.chat.postMessage({
channel: event.channel,
thread_ts: event.ts,
text: `Generated: "${prompt}"`,
});
} catch (error) {
await client.chat.postMessage({
channel: event.channel,
thread_ts: event.ts,
text: `Failed to generate: ${error.message}`,
});
}
});
- Uses the Simulate API for asynchronous video generation
- Supports image-to-video by passing a base64-encoded image
- Polls
getSimulateStatus()until the video is ready - Handles errors gracefully with user feedback