Install
Copy
Ask AI
npm install @odysseyml/odyssey
npm package version
"@odysseyml/odyssey": "^1.0.0" requiredImage-to-Video Requirements
For image-to-video generation:- Max size: 25MB
- Supported formats: JPEG, PNG, WebP, GIF, BMP, HEIC, HEIF, AVIF
- Resolution: Images are automatically resized to 1280x704 (landscape) or 704x1280 (portrait)
HTML
For standalone HTML files without a bundler, import directly from a CDN:Copy
Ask AI
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: system-ui; max-width: 600px; margin: 2rem auto; }
video { width: 100%; background: #000; }
input[type="text"] { flex: 1; padding: 0.5rem; }
button { padding: 0.5rem 1rem; }
.row { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
</style>
</head>
<body>
<video id="video" autoplay playsinline muted></video>
<p id="status">Loading...</p>
<div class="row">
<input id="prompt" type="text" placeholder="Enter prompt..." />
<button id="send">Send</button>
<button id="end" disabled>End</button>
</div>
<script type="module">
import { Odyssey } from 'https://esm.sh/@odysseyml/odyssey';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
const status = document.getElementById('status');
const prompt = document.getElementById('prompt');
const endBtn = document.getElementById('end');
let isStreaming = false;
window.addEventListener('beforeunload', () => client.disconnect());
status.textContent = 'Connecting...';
const mediaStream = await client.connect();
document.getElementById('video').srcObject = mediaStream;
status.textContent = 'Connected';
document.getElementById('send').onclick = async () => {
const text = prompt.value.trim();
if (!text) return;
prompt.value = '';
if (isStreaming) {
await client.interact({ prompt: text });
} else {
status.textContent = 'Starting...';
await client.startStream({ prompt: text });
isStreaming = true;
endBtn.disabled = false;
status.textContent = 'Streaming';
}
};
endBtn.onclick = async () => {
await client.endStream();
isStreaming = false;
endBtn.disabled = true;
status.textContent = 'Connected';
};
</script>
</body>
</html>
Callback Style
Callback Style
Copy
Ask AI
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: system-ui; max-width: 600px; margin: 2rem auto; }
video { width: 100%; background: #000; }
input[type="text"] { flex: 1; padding: 0.5rem; }
button { padding: 0.5rem 1rem; }
.row { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
</style>
</head>
<body>
<video id="video" autoplay playsinline muted></video>
<p id="status">Disconnected</p>
<div class="row">
<button id="connect">Connect</button>
<input id="prompt" type="text" placeholder="Enter prompt..." disabled />
<button id="send" disabled>Send</button>
<button id="end" disabled>End</button>
</div>
<script type="module">
import { Odyssey } from 'https://esm.sh/@odysseyml/odyssey';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
const status = document.getElementById('status');
const prompt = document.getElementById('prompt');
const sendBtn = document.getElementById('send');
const endBtn = document.getElementById('end');
let isStreaming = false;
window.addEventListener('beforeunload', () => client.disconnect());
document.getElementById('connect').onclick = () => {
status.textContent = 'Connecting...';
client.connect({
onConnected: (mediaStream) => {
document.getElementById('video').srcObject = mediaStream;
prompt.disabled = false;
sendBtn.disabled = false;
status.textContent = 'Connected';
},
onStreamStarted: () => {
isStreaming = true;
endBtn.disabled = false;
status.textContent = 'Streaming';
},
onStreamEnded: () => {
isStreaming = false;
endBtn.disabled = true;
status.textContent = 'Connected';
},
onError: (error) => console.error('Error:', error.message),
});
};
sendBtn.onclick = () => {
const text = prompt.value.trim();
if (!text) return;
prompt.value = '';
isStreaming ? client.interact({ prompt: text }) : client.startStream({ prompt: text });
};
endBtn.onclick = () => client.endStream();
</script>
</body>
</html>
Always ensure
disconnect() is called when done (via page unload handlers or component cleanup). Stale connections count towards your concurrent session limit (max 1), which will block new connections until they time out. If disconnect() is not called, connections are automatically cleared after 40 seconds on the server side.JavaScript
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
const mediaStream = await client.connect();
document.querySelector('video').srcObject = mediaStream;
await client.startStream({ prompt: 'A cat' });
await client.interact({ prompt: 'Pet the cat' });
await client.endStream();
client.disconnect();
Callback Style
Callback Style
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
client.connect({
onConnected: (mediaStream) => {
document.querySelector('video').srcObject = mediaStream;
client.startStream({ prompt: 'A cat' });
},
onStreamStarted: () => {
client.interact({ prompt: 'Pet the cat' });
},
onInteractAcknowledged: () => {
client.endStream();
},
onStreamEnded: () => {
client.disconnect();
},
onError: (error, fatal) => {
console.error('Error:', error.message, 'Fatal:', fatal);
},
});
React
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
import { useRef, useEffect, useState } from 'react';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
function App() {
const videoRef = useRef<HTMLVideoElement>(null);
const [prompt, setPrompt] = useState('');
const [status, setStatus] = useState('Disconnected');
const [isConnected, setIsConnected] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
useEffect(() => {
return () => client.disconnect();
}, []);
const handleConnect = async () => {
setStatus('Connecting...');
const mediaStream = await client.connect();
if (videoRef.current) videoRef.current.srcObject = mediaStream;
setIsConnected(true);
setStatus('Connected');
};
const handleSend = async () => {
if (!prompt.trim()) return;
const text = prompt;
setPrompt('');
if (isStreaming) {
await client.interact({ prompt: text });
} else {
setStatus('Starting...');
await client.startStream({ prompt: text });
setIsStreaming(true);
setStatus('Streaming');
}
};
return (
<div>
<video ref={videoRef} autoPlay playsInline muted />
<p>Status: {status}</p>
<button onClick={handleConnect} disabled={isConnected}>Connect</button>
<input value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Enter prompt..." disabled={!isConnected} />
<button onClick={handleSend} disabled={!isConnected}>Send</button>
<button onClick={() => client.endStream()} disabled={!isStreaming}>End</button>
</div>
);
}
Callback Style
Callback Style
Copy
Ask AI
import { Odyssey } from '@odysseyml/odyssey';
import { useRef, useEffect, useState } from 'react';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
function App() {
const videoRef = useRef<HTMLVideoElement>(null);
const [prompt, setPrompt] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
useEffect(() => {
return () => client.disconnect();
}, []);
const handleConnect = () => {
client.connect({
onConnected: (mediaStream) => {
if (videoRef.current) videoRef.current.srcObject = mediaStream;
setIsConnected(true);
},
onStreamStarted: () => setIsStreaming(true),
onStreamEnded: () => setIsStreaming(false),
onError: (error) => console.error('Error:', error.message),
});
};
const handleSend = () => {
const text = prompt;
setPrompt('');
isStreaming
? client.interact({ prompt: text })
: client.startStream({ prompt: text });
};
return (
<div>
<video ref={videoRef} autoPlay playsInline muted />
<button onClick={handleConnect} disabled={isConnected}>Connect</button>
<input value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Enter prompt..." disabled={!isConnected} />
<button onClick={handleSend} disabled={!isConnected}>Send</button>
<button onClick={() => client.endStream()} disabled={!isStreaming}>End</button>
</div>
);
}
Hook Style (useOdyssey)
Hook Style (useOdyssey)
Copy
Ask AI
import { useOdyssey } from '@odysseyml/odyssey/react';
import { useRef, useEffect, useState } from 'react';
function App() {
const videoRef = useRef<HTMLVideoElement>(null);
const [prompt, setPrompt] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
const odyssey = useOdyssey({
apiKey: 'ody_your_api_key_here',
handlers: {
onConnected: (mediaStream) => {
if (videoRef.current) videoRef.current.srcObject = mediaStream;
setIsConnected(true);
},
onStreamStarted: () => setIsStreaming(true),
onStreamEnded: () => setIsStreaming(false),
onDisconnected: () => {
setIsConnected(false);
setIsStreaming(false);
},
onError: (error) => console.error('Error:', error.message),
},
});
useEffect(() => {
return () => odyssey.disconnect();
}, []);
const handleSend = () => {
const text = prompt;
setPrompt('');
isStreaming
? odyssey.interact({ prompt: text })
: odyssey.startStream({ prompt: text });
};
return (
<div>
<video ref={videoRef} autoPlay playsInline muted />
<p>Status: {odyssey.status}</p>
<button onClick={() => odyssey.connect()} disabled={isConnected}>Connect</button>
<input value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Enter prompt..." disabled={!isConnected} />
<button onClick={handleSend} disabled={!isConnected}>Send</button>
<button onClick={() => odyssey.endStream()} disabled={!isStreaming}>End</button>
</div>
);
}
Next.js
Copy
Ask AI
'use client';
import { Odyssey } from '@odysseyml/odyssey';
import { useRef, useEffect, useState } from 'react';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
export default function OdysseyDemo() {
const videoRef = useRef<HTMLVideoElement>(null);
const [prompt, setPrompt] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
useEffect(() => () => client.disconnect(), []);
const handleConnect = async () => {
const mediaStream = await client.connect();
if (videoRef.current) videoRef.current.srcObject = mediaStream;
setIsConnected(true);
};
const handleSend = async () => {
const text = prompt;
setPrompt('');
if (isStreaming) {
await client.interact({ prompt: text });
} else {
await client.startStream({ prompt: text });
setIsStreaming(true);
}
};
return (
<div>
<video ref={videoRef} autoPlay playsInline muted style={{ width: '100%', background: '#000' }} />
<button onClick={handleConnect} disabled={isConnected}>Connect</button>
<input value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Enter prompt..." disabled={!isConnected} />
<button onClick={handleSend} disabled={!isConnected}>Send</button>
<button onClick={() => { client.endStream(); setIsStreaming(false); }} disabled={!isStreaming}>End</button>
</div>
);
}
Callback Style
Callback Style
Copy
Ask AI
'use client';
import { Odyssey } from '@odysseyml/odyssey';
import { useRef, useEffect, useState } from 'react';
const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
export default function OdysseyDemo() {
const videoRef = useRef<HTMLVideoElement>(null);
const [prompt, setPrompt] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
useEffect(() => () => client.disconnect(), []);
const handleConnect = () => {
client.connect({
onConnected: (mediaStream) => {
if (videoRef.current) videoRef.current.srcObject = mediaStream;
setIsConnected(true);
},
onStreamStarted: () => setIsStreaming(true),
onStreamEnded: () => setIsStreaming(false),
onError: (error) => console.error('Error:', error.message),
});
};
const handleSend = () => {
const text = prompt;
setPrompt('');
isStreaming
? client.interact({ prompt: text })
: client.startStream({ prompt: text });
};
return (
<div>
<video ref={videoRef} autoPlay playsInline muted style={{ width: '100%', background: '#000' }} />
<button onClick={handleConnect} disabled={isConnected}>Connect</button>
<input value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Enter prompt..." disabled={!isConnected} />
<button onClick={handleSend} disabled={!isConnected}>Send</button>
<button onClick={() => client.endStream()} disabled={!isStreaming}>End</button>
</div>
);
}
Hook Style (useOdyssey)
Hook Style (useOdyssey)
Copy
Ask AI
'use client';
import { useOdyssey } from '@odysseyml/odyssey/react';
import { useRef, useEffect, useState } from 'react';
export default function OdysseyDemo() {
const videoRef = useRef<HTMLVideoElement>(null);
const [prompt, setPrompt] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
const odyssey = useOdyssey({
apiKey: 'ody_your_api_key_here',
handlers: {
onConnected: (mediaStream) => {
if (videoRef.current) videoRef.current.srcObject = mediaStream;
setIsConnected(true);
},
onStreamStarted: () => setIsStreaming(true),
onStreamEnded: () => setIsStreaming(false),
onDisconnected: () => {
setIsConnected(false);
setIsStreaming(false);
},
onError: (error) => console.error('Error:', error.message),
},
});
useEffect(() => () => odyssey.disconnect(), []);
const handleSend = () => {
const text = prompt;
setPrompt('');
isStreaming
? odyssey.interact({ prompt: text })
: odyssey.startStream({ prompt: text });
};
return (
<div>
<video ref={videoRef} autoPlay playsInline muted style={{ width: '100%', background: '#000' }} />
<p>Status: {odyssey.status}</p>
<button onClick={() => odyssey.connect()} disabled={isConnected}>Connect</button>
<input value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Enter prompt..." disabled={!isConnected} />
<button onClick={handleSend} disabled={!isConnected}>Send</button>
<button onClick={() => odyssey.endStream()} disabled={!isStreaming}>End</button>
</div>
);
}
Next.js App Router requires the
'use client' directive for components using React hooks and browser APIs like video elements.Python
Copy
Ask AI
import asyncio
from odyssey import Odyssey, OdysseyAuthError, OdysseyConnectionError
async def main():
client = Odyssey(api_key="ody_your_api_key_here")
try:
await client.connect(
on_video_frame=lambda frame: print(f"Frame: {frame.width}x{frame.height}"),
on_stream_started=lambda stream_id: print(f"Ready: {stream_id}"),
)
await client.start_stream("A cat", portrait=True)
await client.interact("Pet the cat")
await client.end_stream()
except OdysseyAuthError:
print("Invalid API key")
except OdysseyConnectionError as e:
print(f"Connection failed: {e}")
finally:
await client.disconnect()
asyncio.run(main())
Next Steps
Discord Community
Get help and share what you’re building.