simonw / simonw/tools

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

Open Beginner friendly
#50 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
HTML
Stars
1.9k
Forks
191
Avg merge
36m
Merged PRs (30d)
12

Description

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>

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

In gpt-4o-audio-player.html, start by comparing the current gist-fetching logic with the raw-Gist implementation and the supplied public gist URL. Verify the obsolete PAT expectation is no longer required; done means the public raw gist loads and renders its audio and transcript without authentication.

Written by the indexing model from the issue text.

Assessment

Tech stack
github, javascript
Domain
frontend, web-dev
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.