|
67 | 67 | color: #666; |
68 | 68 | font-style: italic; |
69 | 69 | } |
| 70 | + .progress { |
| 71 | + margin: 10px 0; |
| 72 | + padding: 10px; |
| 73 | + background: #f0f0f0; |
| 74 | + border-radius: 4px; |
| 75 | + } |
70 | 76 | </style> |
71 | 77 | </head> |
72 | 78 | <body> |
|
80 | 86 | <button id="fetchBtn">Fetch</button> |
81 | 87 | </div> |
82 | 88 | <div id="error" class="error" style="display: none;"></div> |
| 89 | + <div id="progress" class="progress" style="display: none;"></div> |
83 | 90 | <div id="playerContainer" class="player-container" style="display: none;"> |
84 | 91 | <audio id="audioPlayer" controls></audio> |
85 | 92 | <button id="downloadBtn">Download Audio</button> |
|
90 | 97 | const gistInput = document.getElementById('gistUrl'); |
91 | 98 | const fetchBtn = document.getElementById('fetchBtn'); |
92 | 99 | const errorDiv = document.getElementById('error'); |
| 100 | + const progressDiv = document.getElementById('progress'); |
93 | 101 | const playerContainer = document.getElementById('playerContainer'); |
94 | 102 | const audioPlayer = document.getElementById('audioPlayer'); |
95 | 103 | const downloadBtn = document.getElementById('downloadBtn'); |
|
101 | 109 | playerContainer.style.display = 'none'; |
102 | 110 | } |
103 | 111 |
|
| 112 | + function showProgress(message) { |
| 113 | + progressDiv.textContent = message; |
| 114 | + progressDiv.style.display = 'block'; |
| 115 | + } |
| 116 | + |
| 117 | + function hideProgress() { |
| 118 | + progressDiv.style.display = 'none'; |
| 119 | + } |
| 120 | + |
104 | 121 | function clearError() { |
105 | 122 | errorDiv.style.display = 'none'; |
106 | 123 | } |
|
118 | 135 | history.pushState({}, '', newUrl); |
119 | 136 | } |
120 | 137 |
|
| 138 | + async function fetchWithRetry(url, options = {}, retries = 3) { |
| 139 | + for (let i = 0; i < retries; i++) { |
| 140 | + try { |
| 141 | + const response = await fetch(url, options); |
| 142 | + if (!response.ok) { |
| 143 | + throw new Error(`HTTP error! status: ${response.status}`); |
| 144 | + } |
| 145 | + return response; |
| 146 | + } catch (error) { |
| 147 | + if (i === retries - 1) throw error; |
| 148 | + await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); |
| 149 | + } |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + async function fetchGistContent(gistId) { |
| 154 | + showProgress('Fetching Gist metadata...'); |
| 155 | + const gistResponse = await fetchWithRetry(`https://api.github.com/gists/${gistId}`); |
| 156 | + const gistData = await gistResponse.json(); |
| 157 | + |
| 158 | + const files = gistData.files; |
| 159 | + if (!files || Object.keys(files).length === 0) { |
| 160 | + throw new Error('No files found in Gist'); |
| 161 | + } |
| 162 | + |
| 163 | + const firstFile = Object.values(files)[0]; |
| 164 | + |
| 165 | + // Check if content is truncated |
| 166 | + if (firstFile.truncated) { |
| 167 | + showProgress('Content is truncated, fetching raw content...'); |
| 168 | + const rawResponse = await fetchWithRetry(firstFile.raw_url); |
| 169 | + return await rawResponse.text(); |
| 170 | + } |
| 171 | + |
| 172 | + return firstFile.content; |
| 173 | + } |
| 174 | + |
121 | 175 | async function processGistUrl(url) { |
122 | 176 | try { |
123 | 177 | fetchBtn.disabled = true; |
| 178 | + clearError(); |
| 179 | + hideProgress(); |
| 180 | + |
124 | 181 | const gistId = extractGistId(url); |
125 | 182 | if (!gistId) throw new Error('Invalid Gist URL'); |
126 | 183 |
|
127 | | - // Update URL with gist ID |
128 | 184 | updateURL(gistId); |
129 | | - |
130 | | - // Update input field if it doesn't match |
131 | 185 | if (gistInput.value !== url) { |
132 | 186 | gistInput.value = url; |
133 | 187 | } |
134 | 188 |
|
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'); |
| 189 | + const content = await fetchGistContent(gistId); |
138 | 190 |
|
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'); |
| 191 | + showProgress('Parsing JSON content...'); |
| 192 | + let jsonContent; |
| 193 | + try { |
| 194 | + jsonContent = JSON.parse(content); |
| 195 | + } catch (e) { |
| 196 | + throw new Error('Failed to parse JSON content. The content might be corrupted or incomplete.'); |
143 | 197 | } |
144 | 198 |
|
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) { |
| 199 | + if (!jsonContent?.choices?.[0]?.message?.audio?.data) { |
151 | 200 | throw new Error('This Gist does not contain a valid GPT-4 audio response'); |
152 | 201 | } |
153 | 202 |
|
154 | | - // Extract audio data and transcript |
155 | | - const audioData = content.choices[0].message.audio.data; |
156 | | - const transcript = content.choices[0].message.audio.transcript; |
| 203 | + showProgress('Processing audio data...'); |
| 204 | + const audioData = jsonContent.choices[0].message.audio.data; |
| 205 | + const transcript = jsonContent.choices[0].message.audio.transcript; |
157 | 206 |
|
158 | | - // Create audio blob and URL |
159 | 207 | const binaryData = atob(audioData); |
160 | 208 | const arrayBuffer = new ArrayBuffer(binaryData.length); |
161 | 209 | const uint8Array = new Uint8Array(arrayBuffer); |
|
165 | 213 | const blob = new Blob([uint8Array], { type: 'audio/wav' }); |
166 | 214 | const audioUrl = URL.createObjectURL(blob); |
167 | 215 |
|
168 | | - // Update UI |
169 | 216 | audioPlayer.src = audioUrl; |
170 | 217 | transcriptDiv.textContent = transcript; |
171 | 218 | playerContainer.style.display = 'block'; |
172 | | - clearError(); |
| 219 | + hideProgress(); |
173 | 220 |
|
174 | | - // Set up download button |
175 | 221 | downloadBtn.onclick = () => { |
176 | 222 | const a = document.createElement('a'); |
177 | 223 | a.href = audioUrl; |
|
184 | 230 | showError(error.message || 'An error occurred'); |
185 | 231 | } finally { |
186 | 232 | fetchBtn.disabled = false; |
| 233 | + hideProgress(); |
187 | 234 | } |
188 | 235 | } |
189 | 236 |
|
190 | | - // Handle form submission |
191 | 237 | fetchBtn.addEventListener('click', () => { |
192 | 238 | const url = gistInput.value.trim(); |
193 | 239 | if (!url) { |
|
203 | 249 | } |
204 | 250 | }); |
205 | 251 |
|
206 | | - // Check for gist ID in URL on page load |
207 | 252 | window.addEventListener('load', () => { |
208 | 253 | const params = new URLSearchParams(window.location.search); |
209 | 254 | const gistId = params.get('gist'); |
210 | 255 | if (gistId) { |
211 | | - // If it's just an ID, construct the full URL |
212 | 256 | const fullUrl = `https://gist.github.com/${gistId}`; |
213 | 257 | gistInput.value = fullUrl; |
214 | 258 | processGistUrl(fullUrl); |
215 | 259 | } |
216 | 260 | }); |
217 | 261 |
|
218 | | - // Handle browser back/forward |
219 | 262 | window.addEventListener('popstate', () => { |
220 | 263 | const params = new URLSearchParams(window.location.search); |
221 | 264 | const gistId = params.get('gist'); |
222 | 265 | if (gistId) { |
223 | 266 | const fullUrl = `https://gist.github.com/${gistId}`; |
224 | 267 | processGistUrl(fullUrl); |
225 | 268 | } else { |
226 | | - // Clear everything if there's no gist ID |
227 | 269 | gistInput.value = ''; |
228 | 270 | playerContainer.style.display = 'none'; |
229 | 271 | clearError(); |
|
0 commit comments