Skip to main content

Install

npm install @odysseyml/odyssey
npm package version "@odysseyml/odyssey": "^0.3.0" required

HTML

For standalone HTML files without a bundler, import directly from a CDN:
<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: system-ui; max-width: 600px; margin: 2rem auto; }
    video { width: 100%; background: #000; }
    input { 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" placeholder="Enter stream 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;

    // Cleanup on page unload
    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(text);
      } else {
        status.textContent = 'Starting...';
        await client.startStream(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>
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

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('A cat');
await client.interact('Make it jump');
await client.endStream();
client.disconnect();

React

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);

  // Cleanup on component unmount
  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(text);
    } else {
      setStatus('Starting...');
      await client.startStream(text);
      setIsStreaming(true);
      setStatus('Streaming');
    }
  };

  const handleEnd = async () => {
    await client.endStream();
    setIsStreaming(false);
    setStatus('Connected');
  };

  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 stream prompt..." disabled={!isConnected} />
      <button onClick={handleSend} disabled={!isConnected}>Send</button>
      <button onClick={handleEnd} disabled={!isStreaming}>End</button>
    </div>
  );
}

Next.js

'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 [status, setStatus] = useState('Disconnected');
  const [isConnected, setIsConnected] = useState(false);
  const [isStreaming, setIsStreaming] = useState(false);

  // Cleanup on component unmount
  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(text);
    } else {
      setStatus('Starting...');
      await client.startStream(text);
      setIsStreaming(true);
      setStatus('Streaming');
    }
  };

  const handleEnd = async () => {
    await client.endStream();
    setIsStreaming(false);
    setStatus('Connected');
  };

  return (
    <div>
      <video ref={videoRef} autoPlay playsInline muted style={{ width: '100%', background: '#000' }} />
      <p>Status: {status}</p>
      <button onClick={handleConnect} disabled={isConnected} style={{ padding: '0.5rem 1rem', marginRight: '0.5rem' }}>
        Connect
      </button>
      <input
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        placeholder="Enter stream prompt..."
        disabled={!isConnected}
        style={{ padding: '0.5rem', marginRight: '0.5rem' }}
      />
      <button onClick={handleSend} disabled={!isConnected} style={{ padding: '0.5rem 1rem', marginRight: '0.5rem' }}>Send</button>
      <button onClick={handleEnd} disabled={!isStreaming} style={{ padding: '0.5rem 1rem' }}>End</button>
    </div>
  );
}
Next.js App Router requires the 'use client' directive for components using React hooks and browser APIs like video elements.

Python

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.