Skip to main content

Install

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

Image-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:
<!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>
<!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

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

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

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

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.