Skip to content

Commit e92642e

Browse files
authored
1 parent 457c6de commit e92642e

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed

gpt-4o-audio-player.html

+234
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Gist Audio Player</title>
7+
<style>
8+
body {
9+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10+
max-width: 800px;
11+
margin: 20px auto;
12+
padding: 0 20px;
13+
line-height: 1.6;
14+
}
15+
.info {
16+
background: #e8f4ff;
17+
padding: 15px;
18+
border-radius: 4px;
19+
margin: 20px 0;
20+
border-left: 4px solid #0066cc;
21+
}
22+
.input-group {
23+
display: flex;
24+
gap: 8px;
25+
margin-bottom: 20px;
26+
}
27+
input {
28+
flex: 1;
29+
padding: 8px 12px;
30+
font-size: 16px;
31+
border: 1px solid #ccc;
32+
border-radius: 4px;
33+
}
34+
button {
35+
padding: 8px 16px;
36+
font-size: 16px;
37+
background: #0066cc;
38+
color: white;
39+
border: none;
40+
border-radius: 4px;
41+
cursor: pointer;
42+
}
43+
button:disabled {
44+
background: #cccccc;
45+
}
46+
button:hover:not(:disabled) {
47+
background: #0055aa;
48+
}
49+
.error {
50+
color: #cc0000;
51+
margin: 10px 0;
52+
}
53+
.player-container {
54+
margin: 20px 0;
55+
}
56+
audio {
57+
width: 100%;
58+
margin: 10px 0;
59+
}
60+
.transcript {
61+
background: #f5f5f5;
62+
padding: 15px;
63+
border-radius: 4px;
64+
margin: 10px 0;
65+
}
66+
.loading {
67+
color: #666;
68+
font-style: italic;
69+
}
70+
</style>
71+
</head>
72+
<body>
73+
<div class="info">
74+
Note: This player expects GitHub Gists containing JSON responses from the OpenAI GPT-4 with audio preview model
75+
(<code>gpt-4o-audio-preview</code>). The JSON should include an audio response with base64-encoded WAV data.
76+
</div>
77+
78+
<div class="input-group">
79+
<input type="url" id="gistUrl" placeholder="Enter Gist URL" aria-label="Gist URL">
80+
<button id="fetchBtn">Fetch</button>
81+
</div>
82+
<div id="error" class="error" style="display: none;"></div>
83+
<div id="playerContainer" class="player-container" style="display: none;">
84+
<audio id="audioPlayer" controls></audio>
85+
<button id="downloadBtn">Download Audio</button>
86+
<div id="transcript" class="transcript"></div>
87+
</div>
88+
89+
<script>
90+
const gistInput = document.getElementById('gistUrl');
91+
const fetchBtn = document.getElementById('fetchBtn');
92+
const errorDiv = document.getElementById('error');
93+
const playerContainer = document.getElementById('playerContainer');
94+
const audioPlayer = document.getElementById('audioPlayer');
95+
const downloadBtn = document.getElementById('downloadBtn');
96+
const transcriptDiv = document.getElementById('transcript');
97+
98+
function showError(message) {
99+
errorDiv.textContent = message;
100+
errorDiv.style.display = 'block';
101+
playerContainer.style.display = 'none';
102+
}
103+
104+
function clearError() {
105+
errorDiv.style.display = 'none';
106+
}
107+
108+
function extractGistId(url) {
109+
if (!url) return null;
110+
// Handle both full URLs and just IDs
111+
const parts = url.split('/');
112+
return parts[parts.length - 1];
113+
}
114+
115+
function updateURL(gistId) {
116+
const newUrl = new URL(window.location);
117+
newUrl.searchParams.set('gist', gistId);
118+
history.pushState({}, '', newUrl);
119+
}
120+
121+
async function processGistUrl(url) {
122+
try {
123+
fetchBtn.disabled = true;
124+
const gistId = extractGistId(url);
125+
if (!gistId) throw new Error('Invalid Gist URL');
126+
127+
// Update URL with gist ID
128+
updateURL(gistId);
129+
130+
// Update input field if it doesn't match
131+
if (gistInput.value !== url) {
132+
gistInput.value = url;
133+
}
134+
135+
// Fetch gist data
136+
const gistResponse = await fetch(`https://api.github.com/gists/${gistId}`);
137+
if (!gistResponse.ok) throw new Error('Failed to fetch Gist');
138+
139+
const gistData = await gistResponse.json();
140+
const files = gistData.files;
141+
if (!files || Object.keys(files).length === 0) {
142+
throw new Error('No files found in Gist');
143+
}
144+
145+
// Get content from first file
146+
const firstFile = Object.values(files)[0];
147+
const content = JSON.parse(firstFile.content);
148+
149+
// Verify this is an audio response
150+
if (!content?.choices?.[0]?.message?.audio?.data) {
151+
throw new Error('This Gist does not contain a valid GPT-4 audio response');
152+
}
153+
154+
// Extract audio data and transcript
155+
const audioData = content.choices[0].message.audio.data;
156+
const transcript = content.choices[0].message.audio.transcript;
157+
158+
// Create audio blob and URL
159+
const binaryData = atob(audioData);
160+
const arrayBuffer = new ArrayBuffer(binaryData.length);
161+
const uint8Array = new Uint8Array(arrayBuffer);
162+
for (let i = 0; i < binaryData.length; i++) {
163+
uint8Array[i] = binaryData.charCodeAt(i);
164+
}
165+
const blob = new Blob([uint8Array], { type: 'audio/wav' });
166+
const audioUrl = URL.createObjectURL(blob);
167+
168+
// Update UI
169+
audioPlayer.src = audioUrl;
170+
transcriptDiv.textContent = transcript;
171+
playerContainer.style.display = 'block';
172+
clearError();
173+
174+
// Set up download button
175+
downloadBtn.onclick = () => {
176+
const a = document.createElement('a');
177+
a.href = audioUrl;
178+
a.download = 'audio.wav';
179+
document.body.appendChild(a);
180+
a.click();
181+
document.body.removeChild(a);
182+
};
183+
} catch (error) {
184+
showError(error.message || 'An error occurred');
185+
} finally {
186+
fetchBtn.disabled = false;
187+
}
188+
}
189+
190+
// Handle form submission
191+
fetchBtn.addEventListener('click', () => {
192+
const url = gistInput.value.trim();
193+
if (!url) {
194+
showError('Please enter a Gist URL');
195+
return;
196+
}
197+
processGistUrl(url);
198+
});
199+
200+
gistInput.addEventListener('keypress', (e) => {
201+
if (e.key === 'Enter') {
202+
fetchBtn.click();
203+
}
204+
});
205+
206+
// Check for gist ID in URL on page load
207+
window.addEventListener('load', () => {
208+
const params = new URLSearchParams(window.location.search);
209+
const gistId = params.get('gist');
210+
if (gistId) {
211+
// If it's just an ID, construct the full URL
212+
const fullUrl = `https://gist.github.com/${gistId}`;
213+
gistInput.value = fullUrl;
214+
processGistUrl(fullUrl);
215+
}
216+
});
217+
218+
// Handle browser back/forward
219+
window.addEventListener('popstate', () => {
220+
const params = new URLSearchParams(window.location.search);
221+
const gistId = params.get('gist');
222+
if (gistId) {
223+
const fullUrl = `https://gist.github.com/${gistId}`;
224+
processGistUrl(fullUrl);
225+
} else {
226+
// Clear everything if there's no gist ID
227+
gistInput.value = '';
228+
playerContainer.style.display = 'none';
229+
clearError();
230+
}
231+
});
232+
</script>
233+
</body>
234+
</html>

0 commit comments

Comments
 (0)