Skip to content

gpt-4o-audio-player.html expects previous github api authentication to fetch public gist #50

@hcarter333

Description

@hcarter333

I wrestled with modifying the code to accept my github PAT before finally asking GPT to rewrite it using the fetch method. There's still a spot to insert a PAT, but it's no longer used. I tested the new code with this gist and it now works for me. The new code expects a URL to the raw gist. Here's the code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gist Audio Player</title>
    <style>
        body {
            font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 800px;
            margin: 20px auto;
            padding: 0 20px;
            line-height: 1.6;
        }
        .info {
            background: #e8f4ff;
            padding: 15px;
            border-radius: 4px;
            margin: 20px 0;
            border-left: 4px solid #0066cc;
        }
        .input-group {
            display: flex;
            gap: 8px;
            margin-bottom: 20px;
        }
        input {
            flex: 1;
            padding: 8px 12px;
            font-size: 16px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        button {
            padding: 8px 16px;
            font-size: 16px;
            background: #0066cc;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:disabled {
            background: #cccccc;
        }
        button:hover:not(:disabled) {
            background: #0055aa;
        }
        .error {
            color: #cc0000;
            margin: 10px 0;
        }
        .player-container {
            margin: 20px 0;
        }
        audio {
            width: 100%;
            margin: 10px 0;
        }
        .transcript {
            background: #f5f5f5;
            padding: 15px;
            border-radius: 4px;
            margin: 10px 0;
        }
        .loading {
            color: #666;
            font-style: italic;
        }
        .progress {
            margin: 10px 0;
            padding: 10px;
            background: #f0f0f0;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <div class="info">
        Note: This player expects GitHub Gists containing JSON responses from the OpenAI GPT-4 with audio preview model 
        (<code>gpt-4o-audio-preview</code>). The JSON should include an audio response with base64-encoded WAV data.
    </div>

    <div class="input-group">
        <input type="url" id="gistUrl" placeholder="Enter Gist URL" aria-label="Gist URL">
        <button id="fetchBtn">Fetch</button>
    </div>
    <div id="error" class="error" style="display: none;"></div>
    <div id="progress" class="progress" style="display: none;"></div>
    <div id="playerContainer" class="player-container" style="display: none;">
        <audio id="audioPlayer" controls></audio>
        <button id="downloadBtn">Download Audio</button>
        <div id="transcript" class="transcript"></div>
    </div>

<script>
  const gistInput = document.getElementById('gistUrl');
  const fetchBtn = document.getElementById('fetchBtn');
  const errorDiv = document.getElementById('error');
  const progressDiv = document.getElementById('progress');
  const playerContainer = document.getElementById('playerContainer');
  const audioPlayer = document.getElementById('audioPlayer');
  const downloadBtn = document.getElementById('downloadBtn');
  const transcriptDiv = document.getElementById('transcript');

  function showError(message) {
    errorDiv.textContent = message;
    errorDiv.style.display = 'block';
    playerContainer.style.display = 'none';
  }

  function showProgress(message) {
    progressDiv.textContent = message;
    progressDiv.style.display = 'block';
  }

  function hideProgress() {
    progressDiv.style.display = 'none';
  }

  function clearError() {
    errorDiv.style.display = 'none';
  }

  // --- Expect and validate a RAW gist URL ---
  function isRawGistUrl(url) {
    try {
      const u = new URL(url);
      // Typical pattern: https://gist.githubusercontent.com/<user>/<gistId>/raw/<commit-or-head>/<filename>
      return (
        u.hostname === 'gist.githubusercontent.com' &&
        u.pathname.includes('/raw/')
      );
    } catch {
      return false;
    }
  }

  // Store the raw URL in the page URL (?raw=<encoded>)
  function updateURLWithRaw(rawUrl) {
    const newUrl = new URL(window.location);
    newUrl.searchParams.set('raw', rawUrl);
    history.pushState({}, '', newUrl);
  }

  async function fetchWithRetry(url, options = {}, retries = 3) {
    for (let i = 0; i < retries; i++) {
      try {
        const response = await fetch(url, options);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response;
      } catch (error) {
        if (i === retries - 1) throw error;
        await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
      }
    }
  }

  // ⚠️ Optional: PAT if you truly need it (e.g., private gists or stricter rate limits).
  // Do NOT put a real token in production frontend code—browsers expose it in DevTools.
  const GITHUB_PAT = ""; // leave empty for public gists

  // Centralized fetch that conditionally adds Authorization if you set GITHUB_PAT
  async function fetchRawGist(url, options = {}) {
    const headers = {
      ...options.headers
    };
    if (GITHUB_PAT) {
      headers['Authorization'] = `Bearer ${GITHUB_PAT}`;
    }
    return fetchWithRetry(url, { ...options, headers });
  }

  async function processRawGistUrl(rawUrl) {
    try {
      fetchBtn.disabled = true;
      clearError();
      hideProgress();

      if (!isRawGistUrl(rawUrl)) {
        throw new Error('Please provide a RAW Gist URL (it should contain "gist.githubusercontent.com/.../raw/...").');
      }

      updateURLWithRaw(rawUrl);
      if (gistInput.value !== rawUrl) {
        gistInput.value = rawUrl;
      }

      showProgress('Fetching raw gist content...');
      const res = await fetchRawGist(rawUrl);
      const content = await res.text();

      showProgress('Parsing JSON content...');
      let jsonContent;
      try {
        jsonContent = JSON.parse(content);
      } catch (e) {
        throw new Error('Failed to parse JSON content. The content might be corrupted or not JSON.');
      }

      // Expect OpenAI-style response with audio data
      const audioData = jsonContent?.choices?.[0]?.message?.audio?.data;
      const transcript = jsonContent?.choices?.[0]?.message?.audio?.transcript;

      if (!audioData) {
        throw new Error('This Gist does not contain a valid GPT-4 audio response (missing base64 audio data).');
      }

      showProgress('Processing audio data...');
      const binaryData = atob(audioData);
      const arrayBuffer = new ArrayBuffer(binaryData.length);
      const uint8Array = new Uint8Array(arrayBuffer);
      for (let i = 0; i < binaryData.length; i++) {
        uint8Array[i] = binaryData.charCodeAt(i);
      }
      const blob = new Blob([uint8Array], { type: 'audio/wav' });
      const audioUrl = URL.createObjectURL(blob);

      audioPlayer.src = audioUrl;
      transcriptDiv.textContent = transcript || '(No transcript provided)';
      playerContainer.style.display = 'block';
      hideProgress();

      downloadBtn.onclick = () => {
        const a = document.createElement('a');
        a.href = audioUrl;
        a.download = 'audio.wav';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      };
    } catch (error) {
      showError(error.message || 'An error occurred');
    } finally {
      fetchBtn.disabled = false;
      hideProgress();
    }
  }

  // --- UI events ---
  fetchBtn.addEventListener('click', () => {
    const url = gistInput.value.trim();
    if (!url) {
      showError('Please enter a RAW Gist URL');
      return;
    }
    processRawGistUrl(url);
  });

  gistInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') {
      fetchBtn.click();
    }
  });

  // On load, support ?raw=<raw-gist-url>
  window.addEventListener('load', () => {
    const params = new URLSearchParams(window.location.search);
    const raw = params.get('raw');
    if (raw) {
      gistInput.value = raw;
      processRawGistUrl(raw);
    }
  });

  window.addEventListener('popstate', () => {
    const params = new URLSearchParams(window.location.search);
    const raw = params.get('raw');
    if (raw) {
      processRawGistUrl(raw);
    } else {
      gistInput.value = '';
      playerContainer.style.display = 'none';
      clearError();
    }
  });
</script>

</body>
</html>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions