> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.api.odyssey.ml/llms.txt
> Use this file to discover all available pages before exploring further.

# API Quick Start

> Integrate Odyssey-2 Pro into your application in 5 minutes.

## Create a developer account & get an API key

Sign up and receive an API key at [developer.odyssey.ml](https://developer.odyssey.ml/dashboard).

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @odysseyml/odyssey
  ```

  ```bash yarn theme={null}
  yarn add @odysseyml/odyssey
  ```

  ```bash pnpm theme={null}
  pnpm add @odysseyml/odyssey
  ```
</CodeGroup>

<Info>
  npm package version `"@odysseyml/odyssey": "^1.0.0" required`
</Info>

### 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:

<CodeGroup>
  ```html Text-to-Video theme={null}
  <!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>
  ```

  ```html Image-to-Video theme={null}
  <!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="image" type="file" accept="image/*" />
    </div>
    <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 imageInput = document.getElementById('image');
      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();
        prompt.value = '';

        if (isStreaming) {
          await client.interact({ prompt: text });
        } else {
          const image = imageInput.files[0]; // Get selected image
          status.textContent = 'Starting...';
          await client.startStream({ prompt: text, image });
          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>
  ```
</CodeGroup>

<AccordionGroup>
  <Accordion title="Callback Style">
    <CodeGroup>
      ```html Text-to-Video theme={null}
      <!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>
      ```

      ```html Image-to-Video theme={null}
      <!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">
          <input id="image" type="file" accept="image/*" />
        </div>
        <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 imageInput = document.getElementById('image');
          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();
            prompt.value = '';
            if (isStreaming) {
              client.interact({ prompt: text });
            } else {
              const image = imageInput.files[0];
              client.startStream({ prompt: text, image });
            }
          };

          endBtn.onclick = () => client.endStream();
        </script>
      </body>
      </html>
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

<Warning>
  Always ensure `disconnect()` is called when done (via page unload handlers or component cleanup). Stale connections count towards your concurrent session limit, which will block new connections until they time out. If `disconnect()` is not called, idle connections are automatically cleared after 15 minutes on the server side.
</Warning>

## JavaScript

<CodeGroup>
  ```typescript Text-to-Video theme={null}
  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();
  ```

  ```typescript Image-to-Video theme={null}
  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;

  // Get image from file input
  const imageFile = document.querySelector('input[type="file"]').files[0];

  await client.startStream({ prompt: 'A cat', image: imageFile });
  await client.interact({ prompt: 'Pet the cat' });
  await client.endStream();
  client.disconnect();
  ```
</CodeGroup>

<AccordionGroup>
  <Accordion title="Callback Style">
    <CodeGroup>
      ```typescript Text-to-Video theme={null}
      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);
        },
      });
      ```

      ```typescript Image-to-Video theme={null}
      import { Odyssey } from '@odysseyml/odyssey';

      const client = new Odyssey({ apiKey: 'ody_your_api_key_here' });
      const imageFile = document.querySelector('input[type="file"]').files[0];

      client.connect({
        onConnected: (mediaStream) => {
          document.querySelector('video').srcObject = mediaStream;
          client.startStream({ prompt: 'A cat', image: imageFile });
        },
        onStreamStarted: () => {
          client.interact({ prompt: 'Pet the cat' });
        },
        onInteractAcknowledged: () => {
          client.endStream();
        },
        onStreamEnded: () => {
          client.disconnect();
        },
        onError: (error, fatal) => {
          console.error('Error:', error.message, 'Fatal:', fatal);
        },
      });
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

## React

<CodeGroup>
  ```tsx Text-to-Video theme={null}
  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>
    );
  }
  ```

  ```tsx Image-to-Video theme={null}
  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 [image, setImage] = useState<File | null>(null);
    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 () => {
      const text = prompt;
      setPrompt('');

      if (isStreaming) {
        await client.interact({ prompt: text });
      } else {
        setStatus('Starting...');
        await client.startStream({ prompt: text, image: image || undefined });
        setIsStreaming(true);
        setStatus('Streaming');
      }
    };

    return (
      <div>
        <video ref={videoRef} autoPlay playsInline muted />
        <p>Status: {status}</p>
        <button onClick={handleConnect} disabled={isConnected}>Connect</button>
        <input type="file" accept="image/*" onChange={(e) => setImage(e.target.files?.[0] || null)} disabled={!isConnected || isStreaming} />
        <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>
    );
  }
  ```
</CodeGroup>

<AccordionGroup>
  <Accordion title="Callback Style">
    <CodeGroup>
      ```tsx Text-to-Video theme={null}
      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>
        );
      }
      ```

      ```tsx Image-to-Video theme={null}
      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 [image, setImage] = useState<File | null>(null);
        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, image: image || undefined });
        };

        return (
          <div>
            <video ref={videoRef} autoPlay playsInline muted />
            <button onClick={handleConnect} disabled={isConnected}>Connect</button>
            <input type="file" accept="image/*" onChange={(e) => setImage(e.target.files?.[0] || null)} disabled={!isConnected || isStreaming} />
            <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>
        );
      }
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Hook Style (useOdyssey)">
    <CodeGroup>
      ```tsx Text-to-Video theme={null}
      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>
        );
      }
      ```

      ```tsx Image-to-Video theme={null}
      import { useOdyssey } from '@odysseyml/odyssey/react';
      import { useRef, useEffect, useState } from 'react';

      function App() {
        const videoRef = useRef<HTMLVideoElement>(null);
        const [prompt, setPrompt] = useState('');
        const [image, setImage] = useState<File | null>(null);
        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, image: image || undefined });
        };

        return (
          <div>
            <video ref={videoRef} autoPlay playsInline muted />
            <p>Status: {odyssey.status}</p>
            <button onClick={() => odyssey.connect()} disabled={isConnected}>Connect</button>
            <input type="file" accept="image/*" onChange={(e) => setImage(e.target.files?.[0] || null)} disabled={!isConnected || isStreaming} />
            <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>
        );
      }
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

## Next.js

<CodeGroup>
  ```tsx Text-to-Video theme={null}
  '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>
    );
  }
  ```

  ```tsx Image-to-Video theme={null}
  '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 [image, setImage] = useState<File | null>(null);
    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, image: image || undefined });
        setIsStreaming(true);
      }
    };

    return (
      <div>
        <video ref={videoRef} autoPlay playsInline muted style={{ width: '100%', background: '#000' }} />
        <button onClick={handleConnect} disabled={isConnected}>Connect</button>
        <input type="file" accept="image/*" onChange={(e) => setImage(e.target.files?.[0] || null)} disabled={!isConnected || isStreaming} />
        <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>
    );
  }
  ```
</CodeGroup>

<AccordionGroup>
  <Accordion title="Callback Style">
    <CodeGroup>
      ```tsx Text-to-Video theme={null}
      '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>
        );
      }
      ```

      ```tsx Image-to-Video theme={null}
      '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 [image, setImage] = useState<File | null>(null);
        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, image: image || undefined });
        };

        return (
          <div>
            <video ref={videoRef} autoPlay playsInline muted style={{ width: '100%', background: '#000' }} />
            <button onClick={handleConnect} disabled={isConnected}>Connect</button>
            <input type="file" accept="image/*" onChange={(e) => setImage(e.target.files?.[0] || null)} disabled={!isConnected || isStreaming} />
            <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>
        );
      }
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Hook Style (useOdyssey)">
    <CodeGroup>
      ```tsx Text-to-Video theme={null}
      '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>
        );
      }
      ```

      ```tsx Image-to-Video theme={null}
      '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 [image, setImage] = useState<File | null>(null);
        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, image: image || undefined });
        };

        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 type="file" accept="image/*" onChange={(e) => setImage(e.target.files?.[0] || null)} disabled={!isConnected || isStreaming} />
            <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>
        );
      }
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

<Info>
  Next.js App Router requires the `'use client'` directive for components using React hooks and browser APIs like video elements.
</Info>

## Python

<CodeGroup>
  ```python Text-to-Video theme={null}
  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())
  ```

  ```python Image-to-Video theme={null}
  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}"),
          )
          # Start with an image file
          await client.start_stream(
              prompt="A cat",
              portrait=False,
              image_path="/path/to/image.jpg"
          )
          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())
  ```
</CodeGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="JavaScript" icon="js" href="sdk/javascript/introduction" />

  <Card title="Python" icon="python" href="sdk/python/introduction" />
</CardGroup>

<Card title="Discord Community" icon="discord" href="https://discord.com/invite/CmV5DgJMAW">
  Get help and share what you're building.
</Card>
