-
-
Notifications
You must be signed in to change notification settings - Fork 153
Open
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>
Metadata
Metadata
Assignees
Labels
No labels