Conversation
📝 WalkthroughWalkthroughThis pull request completely redesigns the ACE-Step Studio HTML interface. The changes introduce a two-pane layout with a controls sidebar and interactive content area, integrate WaveSurfer for audio waveform visualization, implement an audio upload workflow with region-based editing, and add a task submission system with real-time progress tracking and result polling. The UI transitions to a dark theme with a dynamic, mode-driven configuration that supports multiple task types. Approximately 797 lines added and 199 removed in a single, comprehensive rewrite. Changes
Sequence DiagramsequenceDiagram
participant User
participant UI as Client<br/>(UI/WaveSurfer)
participant Server
User->>UI: Upload reference audio
activate UI
UI->>UI: Initialize WaveSurfer<br/>Render waveform
deactivate UI
User->>UI: Select task mode &<br/>configure parameters<br/>(regions, prompts, etc.)
activate UI
UI->>UI: Update conditional<br/>UI sections
deactivate UI
User->>UI: Submit task
activate UI
UI->>UI: Start progress bar
UI->>Server: release_task<br/>(FormData: task config,<br/>audio, regions)
deactivate UI
activate Server
Server-->>UI: Task accepted
deactivate Server
activate UI
loop Poll Status
UI->>Server: query_result (task_id)
activate Server
Server-->>UI: Task status &<br/>progress logs
deactivate Server
UI->>UI: Update progress bar<br/>via log mapping
end
Server-->>UI: Task complete<br/>(output audio)
deactivate UI
activate UI
UI->>UI: Render result card<br/>with player & download
UI->>User: Display results
deactivate UI
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
ui/studio.html (2)
438-442: Revoke object URLs on re-upload/unload to avoid leaks.
URL.createObjectURLshould be revoked when replaced or on unload to prevent memory growth during repeated uploads.♻️ Proposed fix
let wavesurfer = null; let wsRegions = null; let activeRegion = null; let uploadedFile = null; +let currentObjectUrl = null; elements.fileInput.addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; uploadedFile = file; elements.uploadLabel.innerText = "✅ " + file.name; elements.uploadLabel.style.borderColor = "var(--accent)"; elements.uploadLabel.style.color = "var(--accent)"; elements.waveformWrapper.classList.remove('empty'); initWaveSurfer(); - wavesurfer.load(URL.createObjectURL(file)); + if (currentObjectUrl) URL.revokeObjectURL(currentObjectUrl); + currentObjectUrl = URL.createObjectURL(file); + wavesurfer.load(currentObjectUrl); }); + +window.addEventListener('beforeunload', () => { + if (currentObjectUrl) URL.revokeObjectURL(currentObjectUrl); +});Also applies to: 575-585
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/studio.html` around lines 438 - 442, The file creates object URLs for uploads but never revokes them, causing leaks; update the upload handler (where uploadedFile is set) to call URL.revokeObjectURL(uploadedFile) before assigning a new uploadedFile and revoking any previous region-related blobs, and add a window unload handler (or existing cleanup function) to revoke uploadedFile and any region blobs (reference variables uploadedFile, activeRegion, wsRegions, and wavesurfer) so all created object URLs are revoked when replaced or on page unload.
573-573: Remove or implement the no-op resize listener.The current handler schedules an empty callback; either drop it or wire it to real resize handling.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/studio.html` at line 573, The resize listener is a no-op; either remove the window.addEventListener('resize', ...) line or replace the empty setTimeout with a real resize/redraw call for the Wavesurfer instance: detect the wavesurfer symbol and invoke its resize/redraw API (for example, call wavesurfer.drawBuffer() or wavesurfer.drawer.resize()/wavesurfer.params.container rerender logic depending on your Wavesurfer version) and guard with if (wavesurfer) to avoid errors; ensure the handler triggers the UI redraw logic instead of scheduling an empty callback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui/studio.html`:
- Around line 670-715: Before appending repainted region values in the submit
handler, parse elements.regionStart.value and elements.regionEnd.value to
numbers and validate that for taskType 'repaint' or 'lego' the end is >= 0 and
end > start; if the check fails, show an alert (or set
elements.submitBtn.disabled=false and return) to stop submission. Only append
'repainting_start' and 'repainting_end' to formData after the numeric validation
succeeds, and ensure you append numeric values (not raw strings) so backend
receives a valid range.
- Around line 738-783: The current startPolling uses setInterval which can
trigger overlapping async fetches; replace it with a recursive setTimeout loop
inside startPolling that awaits each fetch before scheduling the next timeout
(use a single boolean like "isPolling" or simply chain the next setTimeout call
after processing) to ensure only one in-flight request at a time, preserve the
existing pollInterval and maxTime logic (check Date.now() - startTime and
clear/stop when timeout), keep the same behavior on success/failure by calling
stopSmartProgressBar(), log messages, updateProgressTarget(task.progress_text),
handleSuccess(task) and re-enable elements.submitBtn, and ensure errors are
caught and then the next poll is scheduled (or polling stopped on terminal
states).
- Around line 785-812: The handleSuccess function currently uses card.innerHTML
to inject result.prompt and result.metas (and the URL) which enables DOM XSS;
modify handleSuccess to build the result-card DOM using createElement and
setting textContent for any user/server-provided text (e.g., result.prompt,
metas.bpm/keyscale/duration, elements.prompt fallback) instead of interpolating
into innerHTML, and validate/sanitize fileUrl/fullUrl before assigning to
media/anchor by ensuring the scheme is http or https (reject or neutralize other
schemes), then set audio.src and anchor.href via properties (not via innerHTML)
and append the constructed nodes to card. Ensure you reference handleSuccess,
elements.prompt, result.prompt, result.metas, fileUrl/fullUrl, and getBaseUrl
when making the changes.
- Around line 8-9: Update the floating version script tags to pin to the exact
WaveSurfer.js release and add Subresource Integrity and crossorigin attributes:
replace the src values referencing "https://unpkg.com/wavesurfer.js@7" with the
explicit CDN paths for the released artifacts (e.g.,
"https://unpkg.com/wavesurfer.js@7.12.1/dist/wavesurfer.min.js" and the plugin
"https://unpkg.com/wavesurfer.js@7.12.1/dist/plugins/regions.min.js"), add
integrity="sha384-..." and crossorigin="anonymous" attributes on both <script>
tags, and fetch the correct SRI hashes from unpkg's metadata API by appending
?meta to each package URL before committing.
---
Nitpick comments:
In `@ui/studio.html`:
- Around line 438-442: The file creates object URLs for uploads but never
revokes them, causing leaks; update the upload handler (where uploadedFile is
set) to call URL.revokeObjectURL(uploadedFile) before assigning a new
uploadedFile and revoking any previous region-related blobs, and add a window
unload handler (or existing cleanup function) to revoke uploadedFile and any
region blobs (reference variables uploadedFile, activeRegion, wsRegions, and
wavesurfer) so all created object URLs are revoked when replaced or on page
unload.
- Line 573: The resize listener is a no-op; either remove the
window.addEventListener('resize', ...) line or replace the empty setTimeout with
a real resize/redraw call for the Wavesurfer instance: detect the wavesurfer
symbol and invoke its resize/redraw API (for example, call
wavesurfer.drawBuffer() or
wavesurfer.drawer.resize()/wavesurfer.params.container rerender logic depending
on your Wavesurfer version) and guard with if (wavesurfer) to avoid errors;
ensure the handler triggers the UI redraw logic instead of scheduling an empty
callback.
| <script src="https://unpkg.com/wavesurfer.js@7"></script> | ||
| <script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js"></script> |
There was a problem hiding this comment.
❓ Verification inconclusive
WaveSurfer.js 7.x latest stable version npm release
The latest stable 7.x npm release of wavesurfer.js is 7.12.1 (published 2025-12-04/05). [1][2][3]
npm i wavesurfer.js@^7.12.1Sources: jsDelivr package metadata [1], WebJars/NPM version list [2], SourceForge release mirror notes/timestamp [3].
WaveSurfer.js official documentation CDN SRI integrity crossorigin
WaveSurfer.js official docs show CDN usage via UNPKG (pinned version in the URL), e.g. plugin script tags like: https://unpkg.com/wavesurfer.js@7/dist/plugins/regions.min.js—but they do not provide SRI (integrity) hashes or crossorigin examples in the docs. [1]
How to use WaveSurfer.js from a CDN with SRI + crossorigin
- Pin an exact version and a specific file (no “latest”/range URLs).
- Compute an SRI hash for that exact URL/file, then add:
integrity="sha384-..."crossorigin="anonymous"(required for SRI checks on cross-origin resources) [2][3]
Example (you must replace PASTE_SHA384_BASE64_HERE with the real hash for the exact file you load):
<script
src="https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js"
integrity="sha384-PASTE_SHA384_BASE64_HERE"
crossorigin="anonymous"></script>Notes if you use jsDelivr instead of UNPKG
jsDelivr warns to only use SRI with “static, single-file links” and not with version aliasing, default files, or on-demand minified/combined files (they can change byte-for-byte). [4]
Sources: WaveSurfer docs CDN examples [1]; W3C SRI usage (integrity + crossorigin) [2]; why crossorigin="anonymous" matters for SRI [3]; jsDelivr SRI guidance [4].
unpkg CDN version pinning best practices security SRI
unpkg CDN: version pinning + security (SRI) best practices
-
Pin exact versions (avoid
latestand semver ranges like^18)
unpkg lets you use npm tags and semver ranges, but those can resolve to different code over time and will redirect to a fully-resolved version. For stability and cache hits, use fullpkg@x.y.z+ full file path. [1] -
Prefer “permanent URLs” (exact version) for caching + immutability assumptions
unpkg caches based on the permanent URL containing the package version, relying on npm’s rule that already-published versions can’t be overwritten. [1] -
Always use SRI for third-party scripts/styles
Use the HTMLintegrityattribute so the browser refuses to execute a tampered resource. (SRI supportssha256,sha384,sha512.) [2][3] -
Get SRI hashes from unpkg’s Metadata API (
?meta)
Appending?metato a package directory URL returns JSON metadata including each file’sintegrityvalue. [1] (Example noted here too:...react@16.7.0/...js?meta.) [4] -
Include
crossorigin="anonymous"when using SRI with cross-origin resources
This is part of “correct SRI” usage for CDN-loaded<script>/<link>in common tooling/guidance. [5] -
Treat SRI + pinning as “update requires a deliberate change”
If you upgrade the library version, you must update the URL and the SRI hash, otherwise it will fail to load. [3]
Example (pinned + SRI):
<script
src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"
integrity="sha256-…"
crossorigin="anonymous">
</script>Sources: unpkg docs (versioning, redirects, ?meta integrity) [1]; W3C SRI spec [2]; OWASP SRI overview [3]; unpkg ?meta usage example [4]; webhint SRI correctness checks (incl. crossorigin + recommended hash strength) [5].
Pin exact WaveSurfer.js version and add SRI integrity hash.
The floating @7 tag resolves to different versions over time, bypassing cache immutability and increasing supply-chain risk. Pin to the latest stable version (currently 7.12.1) and add integrity + crossorigin="anonymous" attributes. Retrieve SRI hashes from unpkg's metadata API by appending ?meta to the package URL.
Example:
<script src="https://unpkg.com/wavesurfer.js@7.12.1/dist/wavesurfer.min.js" integrity="sha384-[hash]" crossorigin="anonymous"></script>
<script src="https://unpkg.com/wavesurfer.js@7.12.1/dist/plugins/regions.min.js" integrity="sha384-[hash]" crossorigin="anonymous"></script>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/studio.html` around lines 8 - 9, Update the floating version script tags
to pin to the exact WaveSurfer.js release and add Subresource Integrity and
crossorigin attributes: replace the src values referencing
"https://unpkg.com/wavesurfer.js@7" with the explicit CDN paths for the released
artifacts (e.g., "https://unpkg.com/wavesurfer.js@7.12.1/dist/wavesurfer.min.js"
and the plugin
"https://unpkg.com/wavesurfer.js@7.12.1/dist/plugins/regions.min.js"), add
integrity="sha384-..." and crossorigin="anonymous" attributes on both <script>
tags, and fetch the correct SRI hashes from unpkg's metadata API by appending
?meta to each package URL before committing.
| elements.submitBtn.addEventListener('click', async () => { | ||
| const taskType = elements.taskType.value; | ||
| if (taskType !== 'text2music' && !uploadedFile) { | ||
| alert("Upload an audio file first!"); | ||
| return; | ||
| } | ||
|
|
||
| elements.submitBtn.disabled = true; | ||
| elements.progressContainer.style.display = 'block'; | ||
| elements.logConsole.innerHTML = ''; | ||
| log("Preparing task...", "log-info"); | ||
|
|
||
| // Start the smart animator | ||
| startSmartProgressBar(); | ||
|
|
||
| try { | ||
| const formData = new FormData(); | ||
| formData.append('task_type', taskType); | ||
| formData.append('prompt', elements.prompt.value); | ||
| formData.append('lyrics', elements.lyrics.value); | ||
| formData.append('vocal_language', elements.language.value); | ||
| formData.append('thinking', elements.thinking.checked); | ||
| formData.append('batch_size', 1); | ||
| formData.append('inference_steps', elements.steps.value); | ||
| formData.append('guidance_scale', elements.guidance.value); | ||
| formData.append('audio_duration', elements.duration.value); | ||
|
|
||
| if (taskType === 'cover' || taskType === 'lego') { | ||
| formData.append('audio_cover_strength', elements.strength.value / 100); | ||
| } else { | ||
| formData.append('audio_cover_strength', 1.0); | ||
| } | ||
|
|
||
| if (taskType === 'lego' || taskType === 'extract') { | ||
| formData.append('track_name', elements.trackName.value); | ||
| } | ||
|
|
||
| if (uploadedFile) { | ||
| formData.append('src_audio', uploadedFile); | ||
| } | ||
|
|
||
| if (taskType === 'repaint' || taskType === 'lego') { | ||
| formData.append('repainting_start', elements.regionStart.value); | ||
| formData.append('repainting_end', elements.regionEnd.value); | ||
| } | ||
|
|
There was a problem hiding this comment.
Validate region selection for repaint/lego before submission.
regionEnd defaults to -1, so repaint/lego can submit an invalid range. This can trigger backend errors or undefined output.
🐛 Proposed fix
elements.submitBtn.addEventListener('click', async () => {
const taskType = elements.taskType.value;
if (taskType !== 'text2music' && !uploadedFile) {
alert("Upload an audio file first!");
return;
}
+ if (taskType === 'repaint' || taskType === 'lego') {
+ const start = Number(elements.regionStart.value);
+ const end = Number(elements.regionEnd.value);
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
+ alert("Select a valid region before submitting.");
+ return;
+ }
+ }
elements.submitBtn.disabled = true;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/studio.html` around lines 670 - 715, Before appending repainted region
values in the submit handler, parse elements.regionStart.value and
elements.regionEnd.value to numbers and validate that for taskType 'repaint' or
'lego' the end is >= 0 and end > start; if the check fails, show an alert (or
set elements.submitBtn.disabled=false and return) to stop submission. Only
append 'repainting_start' and 'repainting_end' to formData after the numeric
validation succeeds, and ensure you append numeric values (not raw strings) so
backend receives a valid range.
| async function startPolling(taskId) { | ||
| const pollInterval = 1500; | ||
| const maxWait = 600000; | ||
| const start = Date.now(); | ||
| let list; | ||
|
|
||
| while (Date.now() - start < maxWait) { | ||
| await new Promise(function (r) { setTimeout(r, pollInterval); }); | ||
| list = await queryResult([taskId]); | ||
| if (!list || !list.length) { log('No result in response'); continue; } | ||
| const item = list[0]; | ||
| const status = item.status; | ||
| if (item.progress_text) log(item.progress_text); | ||
| if (status === 1) { | ||
| log('Done.', 'success'); | ||
| const maxTime = 900000; // 15 mins (increased for diffusion) | ||
| const startTime = Date.now(); | ||
|
|
||
| const timer = setInterval(async () => { | ||
| if (Date.now() - startTime > maxTime) { | ||
| clearInterval(timer); | ||
| log("Timed out.", "log-error"); | ||
| stopSmartProgressBar(); | ||
| elements.submitBtn.disabled = false; | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const result = typeof item.result === 'string' ? JSON.parse(item.result) : item.result; | ||
| const arr = Array.isArray(result) ? result : [result]; | ||
| arr.forEach(function (r) { renderResult(taskId, r, base); }); | ||
| const res = await fetch(`${getBaseUrl()}/query_result`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ task_id_list: [taskId] }) | ||
| }); | ||
| const data = await res.json(); | ||
|
|
||
| if (data.code === 200 && data.data && data.data.length > 0) { | ||
| const task = data.data[0]; | ||
|
|
||
| // Update Smart Target | ||
| updateProgressTarget(task.progress_text); | ||
|
|
||
| if (task.status === 1) { // Success | ||
| clearInterval(timer); | ||
| stopSmartProgressBar(); // Force to 100% | ||
| log("Done!", "log-success"); | ||
| handleSuccess(task); | ||
| elements.submitBtn.disabled = false; | ||
| } else if (task.status === 2) { // Failed | ||
| clearInterval(timer); | ||
| stopSmartProgressBar(); | ||
| log("Task Failed.", "log-error"); | ||
| elements.submitBtn.disabled = false; | ||
| } | ||
| } | ||
| } catch (e) { | ||
| log('Parse result: ' + e.message, 'error'); | ||
| console.error("Poll error", e); | ||
| } | ||
| break; | ||
| } | ||
| if (status === 2) { | ||
| log('Task failed.', 'error'); | ||
| break; | ||
| } | ||
| } | ||
| if (Date.now() - start >= maxWait) log('Timeout.', 'error'); | ||
| } catch (e) { | ||
| log(e.message || 'Error', 'error'); | ||
| } | ||
| submitBtn.disabled = false; | ||
| }); | ||
| </script> | ||
| }, pollInterval); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and check the file
find . -name "studio.html" -type fRepository: ace-step/ACE-Step-1.5
Length of output: 81
🏁 Script executed:
# Check line count and read the specific lines
sed -n '738,783p' ./ui/studio.htmlRepository: ace-step/ACE-Step-1.5
Length of output: 1914
Replace setInterval with recursive setTimeout to prevent overlapping polls.
The current code uses setInterval(async () => {...}, 1500), which allows multiple requests to be in-flight simultaneously if the fetch and processing takes longer than 1500ms. This causes responses to potentially arrive out of order and creates race conditions. Use a recursive setTimeout pattern with a flag to ensure only one poll is active at a time.
🐛 Proposed fix
async function startPolling(taskId) {
const pollInterval = 1500;
const maxTime = 900000; // 15 mins (increased for diffusion)
const startTime = Date.now();
-
- const timer = setInterval(async () => {
- if (Date.now() - startTime > maxTime) {
- clearInterval(timer);
- log("Timed out.", "log-error");
- stopSmartProgressBar();
- elements.submitBtn.disabled = false;
- return;
- }
-
- try {
- const res = await fetch(`${getBaseUrl()}/query_result`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ task_id_list: [taskId] })
- });
- const data = await res.json();
-
- if (data.code === 200 && data.data && data.data.length > 0) {
- const task = data.data[0];
-
- // Update Smart Target
- updateProgressTarget(task.progress_text);
-
- if (task.status === 1) { // Success
- clearInterval(timer);
- stopSmartProgressBar(); // Force to 100%
- log("Done!", "log-success");
- handleSuccess(task);
- elements.submitBtn.disabled = false;
- } else if (task.status === 2) { // Failed
- clearInterval(timer);
- stopSmartProgressBar();
- log("Task Failed.", "log-error");
- elements.submitBtn.disabled = false;
- }
- }
- } catch (e) {
- console.error("Poll error", e);
- }
- }, pollInterval);
+ let stopped = false;
+
+ const poll = async () => {
+ if (stopped) return;
+ if (Date.now() - startTime > maxTime) {
+ stopped = true;
+ log("Timed out.", "log-error");
+ stopSmartProgressBar();
+ elements.submitBtn.disabled = false;
+ return;
+ }
+
+ try {
+ const res = await fetch(`${getBaseUrl()}/query_result`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ task_id_list: [taskId] })
+ });
+ const data = await res.json();
+
+ if (data.code === 200 && data.data && data.data.length > 0) {
+ const task = data.data[0];
+
+ // Update Smart Target
+ updateProgressTarget(task.progress_text);
+
+ if (task.status === 1) { // Success
+ stopped = true;
+ stopSmartProgressBar(); // Force to 100%
+ log("Done!", "log-success");
+ handleSuccess(task);
+ elements.submitBtn.disabled = false;
+ return;
+ } else if (task.status === 2) { // Failed
+ stopped = true;
+ stopSmartProgressBar();
+ log("Task Failed.", "log-error");
+ elements.submitBtn.disabled = false;
+ return;
+ }
+ }
+ } catch (e) {
+ console.error("Poll error", e);
+ }
+
+ if (!stopped) setTimeout(poll, pollInterval);
+ };
+
+ poll();
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function startPolling(taskId) { | |
| const pollInterval = 1500; | |
| const maxWait = 600000; | |
| const start = Date.now(); | |
| let list; | |
| while (Date.now() - start < maxWait) { | |
| await new Promise(function (r) { setTimeout(r, pollInterval); }); | |
| list = await queryResult([taskId]); | |
| if (!list || !list.length) { log('No result in response'); continue; } | |
| const item = list[0]; | |
| const status = item.status; | |
| if (item.progress_text) log(item.progress_text); | |
| if (status === 1) { | |
| log('Done.', 'success'); | |
| const maxTime = 900000; // 15 mins (increased for diffusion) | |
| const startTime = Date.now(); | |
| const timer = setInterval(async () => { | |
| if (Date.now() - startTime > maxTime) { | |
| clearInterval(timer); | |
| log("Timed out.", "log-error"); | |
| stopSmartProgressBar(); | |
| elements.submitBtn.disabled = false; | |
| return; | |
| } | |
| try { | |
| const result = typeof item.result === 'string' ? JSON.parse(item.result) : item.result; | |
| const arr = Array.isArray(result) ? result : [result]; | |
| arr.forEach(function (r) { renderResult(taskId, r, base); }); | |
| const res = await fetch(`${getBaseUrl()}/query_result`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ task_id_list: [taskId] }) | |
| }); | |
| const data = await res.json(); | |
| if (data.code === 200 && data.data && data.data.length > 0) { | |
| const task = data.data[0]; | |
| // Update Smart Target | |
| updateProgressTarget(task.progress_text); | |
| if (task.status === 1) { // Success | |
| clearInterval(timer); | |
| stopSmartProgressBar(); // Force to 100% | |
| log("Done!", "log-success"); | |
| handleSuccess(task); | |
| elements.submitBtn.disabled = false; | |
| } else if (task.status === 2) { // Failed | |
| clearInterval(timer); | |
| stopSmartProgressBar(); | |
| log("Task Failed.", "log-error"); | |
| elements.submitBtn.disabled = false; | |
| } | |
| } | |
| } catch (e) { | |
| log('Parse result: ' + e.message, 'error'); | |
| console.error("Poll error", e); | |
| } | |
| break; | |
| } | |
| if (status === 2) { | |
| log('Task failed.', 'error'); | |
| break; | |
| } | |
| } | |
| if (Date.now() - start >= maxWait) log('Timeout.', 'error'); | |
| } catch (e) { | |
| log(e.message || 'Error', 'error'); | |
| } | |
| submitBtn.disabled = false; | |
| }); | |
| </script> | |
| }, pollInterval); | |
| } | |
| async function startPolling(taskId) { | |
| const pollInterval = 1500; | |
| const maxTime = 900000; // 15 mins (increased for diffusion) | |
| const startTime = Date.now(); | |
| let stopped = false; | |
| const poll = async () => { | |
| if (stopped) return; | |
| if (Date.now() - startTime > maxTime) { | |
| stopped = true; | |
| log("Timed out.", "log-error"); | |
| stopSmartProgressBar(); | |
| elements.submitBtn.disabled = false; | |
| return; | |
| } | |
| try { | |
| const res = await fetch(`${getBaseUrl()}/query_result`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ task_id_list: [taskId] }) | |
| }); | |
| const data = await res.json(); | |
| if (data.code === 200 && data.data && data.data.length > 0) { | |
| const task = data.data[0]; | |
| // Update Smart Target | |
| updateProgressTarget(task.progress_text); | |
| if (task.status === 1) { // Success | |
| stopped = true; | |
| stopSmartProgressBar(); // Force to 100% | |
| log("Done!", "log-success"); | |
| handleSuccess(task); | |
| elements.submitBtn.disabled = false; | |
| return; | |
| } else if (task.status === 2) { // Failed | |
| stopped = true; | |
| stopSmartProgressBar(); | |
| log("Task Failed.", "log-error"); | |
| elements.submitBtn.disabled = false; | |
| return; | |
| } | |
| } | |
| } catch (e) { | |
| console.error("Poll error", e); | |
| } | |
| if (!stopped) setTimeout(poll, pollInterval); | |
| }; | |
| poll(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/studio.html` around lines 738 - 783, The current startPolling uses
setInterval which can trigger overlapping async fetches; replace it with a
recursive setTimeout loop inside startPolling that awaits each fetch before
scheduling the next timeout (use a single boolean like "isPolling" or simply
chain the next setTimeout call after processing) to ensure only one in-flight
request at a time, preserve the existing pollInterval and maxTime logic (check
Date.now() - startTime and clear/stop when timeout), keep the same behavior on
success/failure by calling stopSmartProgressBar(), log messages,
updateProgressTarget(task.progress_text), handleSuccess(task) and re-enable
elements.submitBtn, and ensure errors are caught and then the next poll is
scheduled (or polling stopped on terminal states).
| function handleSuccess(task) { | ||
| try { | ||
| let result = task.result; | ||
| if (typeof result === 'string') result = JSON.parse(result); | ||
| if (Array.isArray(result)) result = result[0]; | ||
|
|
||
| const baseUrl = getBaseUrl(); | ||
| const fileUrl = result.url || result.file; | ||
| const fullUrl = fileUrl.startsWith('http') ? fileUrl : `${baseUrl}/${fileUrl.replace(/^\//, '')}`; | ||
|
|
||
| const card = document.createElement('div'); | ||
| card.className = 'result-card'; | ||
|
|
||
| const metas = result.metas || {}; | ||
| const metaHtml = ` | ||
| <div class="result-meta"> | ||
| <strong>${(result.prompt || elements.prompt.value).substring(0, 50)}...</strong><br> | ||
| <span>BPM: ${metas.bpm || '-'} | Key: ${metas.keyscale || '-'} | Dur: ${metas.duration || '-'}s</span> | ||
| </div> | ||
| `; | ||
|
|
||
| card.innerHTML = ` | ||
| ${metaHtml} | ||
| <audio controls src="${fullUrl}"></audio> | ||
| <div style="margin-top:12px; display:flex; gap:10px;"> | ||
| <a href="${fullUrl}" download class="btn" style="padding:10px; font-size:0.8rem; background:#333; text-decoration:none; color:white; text-align:center;">Download</a> | ||
| </div> | ||
| `; |
There was a problem hiding this comment.
Prevent DOM XSS in result rendering.
innerHTML interpolates result.prompt/result.metas which can contain HTML from the server (or user input), enabling DOM XSS. Build DOM nodes with textContent and validate URL schemes.
🛡️ Proposed fix
+function safeMediaUrl(rawUrl, baseUrl) {
+ const url = new URL(rawUrl, baseUrl);
+ if (!['http:', 'https:'].includes(url.protocol)) {
+ throw new Error('Unsupported media URL');
+ }
+ return url.href;
+}
+
function handleSuccess(task) {
try {
let result = task.result;
if (typeof result === 'string') result = JSON.parse(result);
if (Array.isArray(result)) result = result[0];
const baseUrl = getBaseUrl();
const fileUrl = result.url || result.file;
- const fullUrl = fileUrl.startsWith('http') ? fileUrl : `${baseUrl}/${fileUrl.replace(/^\//, '')}`;
-
- const card = document.createElement('div');
- card.className = 'result-card';
-
- const metas = result.metas || {};
- const metaHtml = `
- <div class="result-meta">
- <strong>${(result.prompt || elements.prompt.value).substring(0, 50)}...</strong><br>
- <span>BPM: ${metas.bpm || '-'} | Key: ${metas.keyscale || '-'} | Dur: ${metas.duration || '-'}s</span>
- </div>
- `;
-
- card.innerHTML = `
- ${metaHtml}
- <audio controls src="${fullUrl}"></audio>
- <div style="margin-top:12px; display:flex; gap:10px;">
- <a href="${fullUrl}" download class="btn" style="padding:10px; font-size:0.8rem; background:`#333`; text-decoration:none; color:white; text-align:center;">Download</a>
- </div>
- `;
+ const fullUrl = safeMediaUrl(
+ fileUrl.startsWith('http') ? fileUrl : `${baseUrl}/${fileUrl.replace(/^\//, '')}`,
+ baseUrl
+ );
+
+ const card = document.createElement('div');
+ card.className = 'result-card';
+
+ const metas = result.metas || {};
+ const metaDiv = document.createElement('div');
+ metaDiv.className = 'result-meta';
+ const title = document.createElement('strong');
+ title.textContent = `${(result.prompt || elements.prompt.value).substring(0, 50)}...`;
+ const details = document.createElement('span');
+ details.textContent = `BPM: ${metas.bpm || '-'} | Key: ${metas.keyscale || '-'} | Dur: ${metas.duration || '-'}s`;
+ metaDiv.append(title, document.createElement('br'), details);
+
+ const audio = document.createElement('audio');
+ audio.controls = true;
+ audio.src = fullUrl;
+
+ const actions = document.createElement('div');
+ actions.style.marginTop = '12px';
+ actions.style.display = 'flex';
+ actions.style.gap = '10px';
+ const download = document.createElement('a');
+ download.href = fullUrl;
+ download.download = '';
+ download.className = 'btn';
+ download.style.padding = '10px';
+ download.style.fontSize = '0.8rem';
+ download.style.background = '#333';
+ download.style.textDecoration = 'none';
+ download.style.color = 'white';
+ download.style.textAlign = 'center';
+ download.textContent = 'Download';
+ actions.appendChild(download);
+
+ card.append(metaDiv, audio, actions);
elements.resultsGrid.prepend(card);
} catch (e) {
log("Parsing error: " + e.message, "log-error");
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/studio.html` around lines 785 - 812, The handleSuccess function currently
uses card.innerHTML to inject result.prompt and result.metas (and the URL) which
enables DOM XSS; modify handleSuccess to build the result-card DOM using
createElement and setting textContent for any user/server-provided text (e.g.,
result.prompt, metas.bpm/keyscale/duration, elements.prompt fallback) instead of
interpolating into innerHTML, and validate/sanitize fileUrl/fullUrl before
assigning to media/anchor by ensuring the scheme is http or https (reject or
neutralize other schemes), then set audio.src and anchor.href via properties
(not via innerHTML) and append the constructed nodes to card. Ensure you
reference handleSuccess, elements.prompt, result.prompt, result.metas,
fileUrl/fullUrl, and getBaseUrl when making the changes.
I took the liberty to use all my API enhancement to update the studio UI to a much nice version
Summary by CodeRabbit