Skip to content

feat: wire up voice enhance UI, fix denoise stereo bug - #9

Open
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui
Open

feat: wire up voice enhance UI, fix denoise stereo bug#9
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui

Conversation

@GinoongFlores

Copy link
Copy Markdown

Voice Enhance was backend-only (/api/audio/denoise) with no UI, and had a bug that would've broken it if called anyway.

  • new Enhance tab in the Audio panel: upload a file, denoise it, add to timeline
  • new Voice Enhance section on selected video clips: denoises the clip's own audio in place (mutes original, adds cleaned copy as a new track)
  • fix denoiseAudio() parsing a raw WAV response as JSON
  • fix noisereduce channel-axis mismatch (soundfile gives (frames, channels), noisereduce expects (channels, frames)) causing a 500 on stereo audio

- add Enhance tab (upload+denoise a file) and per-clip Voice Enhance
section (denoise a timeline video clip's own audio in place)
- fix denoiseAudio() parsing raw WAV response as JSON
- fix noisereduce channel-axis mismatch (soundfile gives (frames, channels),
noisereduce expects (channels, frames)) causing 500 on stereo audio

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed — found 5 issues across 3 files. The PR wires up the voice enhance UI in a new EnhanceView tab and a EnhanceAudioSection in video properties, plus fixes a stereo denoising bug in the Python backend. The stereo fix is correct and the denoiseAudio return-type fix from DenoiseResult to Blob resolves a latent broken API call. Architecture conformance is good. All findings are low-severity — three object URL memory leaks, one dead type, and one stale callback closure.

Findings

apps/web/src/components/editor/panels/assets/views/enhance.tsx

  1. Object URL memory leaksURL.createObjectURL() is called on lines 43 and 57 without corresponding URL.revokeObjectURL(). Additionally, handlePickFile (line 28) and the Remove button handler (line 100) nullify resultUrl state without revoking the old blob URL first. Blob URLs accumulate in browser memory until page navigation.

  2. getAudioDuration leaks Audio element — Lines 183–188 create a new Audio element per call but never call remove() or set src = "" after the duration is resolved. The blob URL passed to it is also never revoked.

  3. handlePickFile stale closure — The empty dependency array [] on line 31 means the callback can't access the current resultUrl value to revoke its blob URL. A ref should be used to track the active blob URL.

apps/web/src/components/editor/panels/properties/video-properties.tsx

  1. Object URL memory leak in EnhanceAudioSection — Line 95 creates a blob URL via URL.createObjectURL(denoisedBlob) that is stored as a timeline element's sourceUrl but is never revoked when the element is removed or the function is re-invoked.

apps/web/src/types/ai.ts

  1. Dead DenoiseResult type — Lines 95–98 define DenoiseResult which has no remaining consumers after denoiseAudio was changed to return Blob. The adjacent DenoiseRequest interface (lines 91–93) is also unused.

Verdict

⚠️Changes requested. The stereo fix and API wiring are solid, but the five low-severity issues — particularly the object URL memory leaks and the getAudioDuration cleanup gap — should be addressed before merging to avoid memory accumulation in long SPA editing sessions.


Review with Vorflux

try {
const blob = await aiClient.denoiseAudio(file, strength);
setResultBlob(blob);
setResultUrl(URL.createObjectURL(blob));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(blob) creates a new blob URL on every enhance call, but the previous blob URL (if any) is never revoked via URL.revokeObjectURL(). In a long SPA editing session, this accumulates memory that's only freed on page navigation.

Fix: Before calling setResultUrl(URL.createObjectURL(blob)), revoke the current resultUrl if it exists:

if(resultUrl)URL.revokeObjectURL(resultUrl);

This requires either adding resultUrl to the useCallback deps (also fixing the stale-closure issue noted on line 28) or using a ref.

const handleAddToTimeline = useCallback(async () => {
if (!resultBlob) return;
const outFile = new File([resultBlob], `enhanced_${Date.now()}.wav`, { type: "audio/wav" });
const audioUrl = URL.createObjectURL(outFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(outFile) creates a blob URL stored as the timeline element's sourceUrl. If the user removes the element from the timeline, this blob URL is never revoked. Consider tracking timeline-inserted blob URLs and revoking them when the corresponding element is deleted.


const handlePickFile = useCallback((f: File) => {
setFile(f);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure + missing revoke:handlePickFile has an empty dependency array [], so it can't access the currentresultUrl to call URL.revokeObjectURL() before nullifying it. When the user picks a new file, the previous enhanced preview's blob URL is abandoned without cleanup.

Fix: Use a useRef<string | null> to track the active blob URL so it's always reachable from this callback regardless of the closure.

constresultUrlRef=useRef<string|null>(null);// In handlePickFile:if(resultUrlRef.current)URL.revokeObjectURL(resultUrlRef.current);resultUrlRef.current=null;setResultUrl(null);// After creating URL in handleEnhance:resultUrlRef.current=url;setResultUrl(url);

className="text-[10px] text-destructive hover:text-destructive/80 shrink-0 ml-2"
onClick={() => {
setFile(null);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak: The Remove button handler sets setResultUrl(null) without first revoking the old blob URL via URL.revokeObjectURL(). Same stale-closure issue as handlePickFile — the inline handler can't access the current resultUrl to revoke it. Use the same ref-based approach suggested for handlePickFile on line 28.

);
}

function getAudioDuration(url: string): Promise<number> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio element leak:getAudioDuration creates a new Audio instance on every call but never releases it. On both loadedmetadata and error events, the audio element and its blob URL resource should be cleaned up:

functiongetAudioDuration(url: string): Promise<number>{returnnewPromise((resolve)=>{constaudio=newAudio(url);constcleanup=()=>{audio.removeEventListener("loadedmetadata",onLoaded);audio.removeEventListener("error",onError);audio.src="";audio.remove();if(url.startsWith("blob:"))URL.revokeObjectURL(url);};constonLoaded=()=>{constd=audio.duration;cleanup();resolve(d);};constonError=()=>{cleanup();resolve(5);};audio.addEventListener("loadedmetadata",onLoaded);audio.addEventListener("error",onError);});}

type: "audio/wav",
});
const denoisedBlob = await aiClient.denoiseAudio(file, strength);
const audioUrl = URL.createObjectURL(denoisedBlob);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(denoisedBlob) creates a blob URL that's stored in the timeline element's sourceUrl but never revoked. If the user removes the audio track or re-runs enhance (which would replace the previous blob URL), memory accumulates.

@vorflux

vorfluxBot commented Aug 27, 2026

Copy link
Copy Markdown

Risk Assessment

This PR adds a new Voice Enhance UI feature across two components and fixes a stereo denoising bug in the Python backend. The diff is focused (359 lines, 5 files), the fix is targeted and correct, and the new UI is additive with no changes to existing code paths except the denoiseAudio return type which had no other callers.

Risk score: 2/10 (threshold: 3) — within the configured risk threshold
Overall risk: Low

Concerns

  1. Object URL memory leaks in both new components (EnhanceView and EnhanceAudioSection) — blob URLs accumulate in long SPA sessions without per-URL cleanup. Low severity but worth fixing before heavy user adoption.
  2. getAudioDuration doesn't release the Audio element or its blob URL after resolving — minor resource leak.
  3. No test coverage for the new components or the denoiseAudio method change.

Verdict

Safe to ship. All findings are low-severity and the core logic (stereo fix, API wiring) is correct. Rollback is trivial — revert the PR with no data or migration impact.

vorflux[bot]
vorfluxBot approved these changes Aug 27, 2026

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — the stereo fix is correct and the voice enhance UI wiring is solid. Five low-severity findings (object URL memory leaks, stale closure, dead type) noted inline but none are blockers. Risk score 2/10, within the configured threshold of 3. ✅ Approved.


Review with Vorflux

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@GinoongFlores
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: wire up voice enhance UI, fix denoise stereo bug by GinoongFlores · Pull Request #9 · Ekaanth/OpenCut-AI · GitHub
Skip to content

feat: wire up voice enhance UI, fix denoise stereo bug - #9

Open
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui
Open

feat: wire up voice enhance UI, fix denoise stereo bug#9
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui

Conversation

@GinoongFlores

Copy link
Copy Markdown

Voice Enhance was backend-only (/api/audio/denoise) with no UI, and had a bug that would've broken it if called anyway.

  • new Enhance tab in the Audio panel: upload a file, denoise it, add to timeline
  • new Voice Enhance section on selected video clips: denoises the clip's own audio in place (mutes original, adds cleaned copy as a new track)
  • fix denoiseAudio() parsing a raw WAV response as JSON
  • fix noisereduce channel-axis mismatch (soundfile gives (frames, channels), noisereduce expects (channels, frames)) causing a 500 on stereo audio

- add Enhance tab (upload+denoise a file) and per-clip Voice Enhance
section (denoise a timeline video clip's own audio in place)
- fix denoiseAudio() parsing raw WAV response as JSON
- fix noisereduce channel-axis mismatch (soundfile gives (frames, channels),
noisereduce expects (channels, frames)) causing 500 on stereo audio

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed — found 5 issues across 3 files. The PR wires up the voice enhance UI in a new EnhanceView tab and a EnhanceAudioSection in video properties, plus fixes a stereo denoising bug in the Python backend. The stereo fix is correct and the denoiseAudio return-type fix from DenoiseResult to Blob resolves a latent broken API call. Architecture conformance is good. All findings are low-severity — three object URL memory leaks, one dead type, and one stale callback closure.

Findings

apps/web/src/components/editor/panels/assets/views/enhance.tsx

  1. Object URL memory leaksURL.createObjectURL() is called on lines 43 and 57 without corresponding URL.revokeObjectURL(). Additionally, handlePickFile (line 28) and the Remove button handler (line 100) nullify resultUrl state without revoking the old blob URL first. Blob URLs accumulate in browser memory until page navigation.

  2. getAudioDuration leaks Audio element — Lines 183–188 create a new Audio element per call but never call remove() or set src = "" after the duration is resolved. The blob URL passed to it is also never revoked.

  3. handlePickFile stale closure — The empty dependency array [] on line 31 means the callback can't access the current resultUrl value to revoke its blob URL. A ref should be used to track the active blob URL.

apps/web/src/components/editor/panels/properties/video-properties.tsx

  1. Object URL memory leak in EnhanceAudioSection — Line 95 creates a blob URL via URL.createObjectURL(denoisedBlob) that is stored as a timeline element's sourceUrl but is never revoked when the element is removed or the function is re-invoked.

apps/web/src/types/ai.ts

  1. Dead DenoiseResult type — Lines 95–98 define DenoiseResult which has no remaining consumers after denoiseAudio was changed to return Blob. The adjacent DenoiseRequest interface (lines 91–93) is also unused.

Verdict

⚠️Changes requested. The stereo fix and API wiring are solid, but the five low-severity issues — particularly the object URL memory leaks and the getAudioDuration cleanup gap — should be addressed before merging to avoid memory accumulation in long SPA editing sessions.


Review with Vorflux

try {
const blob = await aiClient.denoiseAudio(file, strength);
setResultBlob(blob);
setResultUrl(URL.createObjectURL(blob));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(blob) creates a new blob URL on every enhance call, but the previous blob URL (if any) is never revoked via URL.revokeObjectURL(). In a long SPA editing session, this accumulates memory that's only freed on page navigation.

Fix: Before calling setResultUrl(URL.createObjectURL(blob)), revoke the current resultUrl if it exists:

if(resultUrl)URL.revokeObjectURL(resultUrl);

This requires either adding resultUrl to the useCallback deps (also fixing the stale-closure issue noted on line 28) or using a ref.

const handleAddToTimeline = useCallback(async () => {
if (!resultBlob) return;
const outFile = new File([resultBlob], `enhanced_${Date.now()}.wav`, { type: "audio/wav" });
const audioUrl = URL.createObjectURL(outFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(outFile) creates a blob URL stored as the timeline element's sourceUrl. If the user removes the element from the timeline, this blob URL is never revoked. Consider tracking timeline-inserted blob URLs and revoking them when the corresponding element is deleted.


const handlePickFile = useCallback((f: File) => {
setFile(f);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure + missing revoke:handlePickFile has an empty dependency array [], so it can't access the currentresultUrl to call URL.revokeObjectURL() before nullifying it. When the user picks a new file, the previous enhanced preview's blob URL is abandoned without cleanup.

Fix: Use a useRef<string | null> to track the active blob URL so it's always reachable from this callback regardless of the closure.

constresultUrlRef=useRef<string|null>(null);// In handlePickFile:if(resultUrlRef.current)URL.revokeObjectURL(resultUrlRef.current);resultUrlRef.current=null;setResultUrl(null);// After creating URL in handleEnhance:resultUrlRef.current=url;setResultUrl(url);

className="text-[10px] text-destructive hover:text-destructive/80 shrink-0 ml-2"
onClick={() => {
setFile(null);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak: The Remove button handler sets setResultUrl(null) without first revoking the old blob URL via URL.revokeObjectURL(). Same stale-closure issue as handlePickFile — the inline handler can't access the current resultUrl to revoke it. Use the same ref-based approach suggested for handlePickFile on line 28.

);
}

function getAudioDuration(url: string): Promise<number> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio element leak:getAudioDuration creates a new Audio instance on every call but never releases it. On both loadedmetadata and error events, the audio element and its blob URL resource should be cleaned up:

functiongetAudioDuration(url: string): Promise<number>{returnnewPromise((resolve)=>{constaudio=newAudio(url);constcleanup=()=>{audio.removeEventListener("loadedmetadata",onLoaded);audio.removeEventListener("error",onError);audio.src="";audio.remove();if(url.startsWith("blob:"))URL.revokeObjectURL(url);};constonLoaded=()=>{constd=audio.duration;cleanup();resolve(d);};constonError=()=>{cleanup();resolve(5);};audio.addEventListener("loadedmetadata",onLoaded);audio.addEventListener("error",onError);});}

type: "audio/wav",
});
const denoisedBlob = await aiClient.denoiseAudio(file, strength);
const audioUrl = URL.createObjectURL(denoisedBlob);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(denoisedBlob) creates a blob URL that's stored in the timeline element's sourceUrl but never revoked. If the user removes the audio track or re-runs enhance (which would replace the previous blob URL), memory accumulates.

@vorflux

vorfluxBot commented Aug 27, 2026

Copy link
Copy Markdown

Risk Assessment

This PR adds a new Voice Enhance UI feature across two components and fixes a stereo denoising bug in the Python backend. The diff is focused (359 lines, 5 files), the fix is targeted and correct, and the new UI is additive with no changes to existing code paths except the denoiseAudio return type which had no other callers.

Risk score: 2/10 (threshold: 3) — within the configured risk threshold
Overall risk: Low

Concerns

  1. Object URL memory leaks in both new components (EnhanceView and EnhanceAudioSection) — blob URLs accumulate in long SPA sessions without per-URL cleanup. Low severity but worth fixing before heavy user adoption.
  2. getAudioDuration doesn't release the Audio element or its blob URL after resolving — minor resource leak.
  3. No test coverage for the new components or the denoiseAudio method change.

Verdict

Safe to ship. All findings are low-severity and the core logic (stereo fix, API wiring) is correct. Rollback is trivial — revert the PR with no data or migration impact.

vorflux[bot]
vorfluxBot approved these changes Aug 27, 2026

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — the stereo fix is correct and the voice enhance UI wiring is solid. Five low-severity findings (object URL memory leaks, stale closure, dead type) noted inline but none are blockers. Risk score 2/10, within the configured threshold of 3. ✅ Approved.


Review with Vorflux

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@GinoongFlores
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: wire up voice enhance UI, fix denoise stereo bug by GinoongFlores · Pull Request #9 · Ekaanth/OpenCut-AI · GitHub
Skip to content

feat: wire up voice enhance UI, fix denoise stereo bug - #9

Open
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui
Open

feat: wire up voice enhance UI, fix denoise stereo bug#9
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui

Conversation

@GinoongFlores

Copy link
Copy Markdown

Voice Enhance was backend-only (/api/audio/denoise) with no UI, and had a bug that would've broken it if called anyway.

  • new Enhance tab in the Audio panel: upload a file, denoise it, add to timeline
  • new Voice Enhance section on selected video clips: denoises the clip's own audio in place (mutes original, adds cleaned copy as a new track)
  • fix denoiseAudio() parsing a raw WAV response as JSON
  • fix noisereduce channel-axis mismatch (soundfile gives (frames, channels), noisereduce expects (channels, frames)) causing a 500 on stereo audio

- add Enhance tab (upload+denoise a file) and per-clip Voice Enhance
section (denoise a timeline video clip's own audio in place)
- fix denoiseAudio() parsing raw WAV response as JSON
- fix noisereduce channel-axis mismatch (soundfile gives (frames, channels),
noisereduce expects (channels, frames)) causing 500 on stereo audio

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed — found 5 issues across 3 files. The PR wires up the voice enhance UI in a new EnhanceView tab and a EnhanceAudioSection in video properties, plus fixes a stereo denoising bug in the Python backend. The stereo fix is correct and the denoiseAudio return-type fix from DenoiseResult to Blob resolves a latent broken API call. Architecture conformance is good. All findings are low-severity — three object URL memory leaks, one dead type, and one stale callback closure.

Findings

apps/web/src/components/editor/panels/assets/views/enhance.tsx

  1. Object URL memory leaksURL.createObjectURL() is called on lines 43 and 57 without corresponding URL.revokeObjectURL(). Additionally, handlePickFile (line 28) and the Remove button handler (line 100) nullify resultUrl state without revoking the old blob URL first. Blob URLs accumulate in browser memory until page navigation.

  2. getAudioDuration leaks Audio element — Lines 183–188 create a new Audio element per call but never call remove() or set src = "" after the duration is resolved. The blob URL passed to it is also never revoked.

  3. handlePickFile stale closure — The empty dependency array [] on line 31 means the callback can't access the current resultUrl value to revoke its blob URL. A ref should be used to track the active blob URL.

apps/web/src/components/editor/panels/properties/video-properties.tsx

  1. Object URL memory leak in EnhanceAudioSection — Line 95 creates a blob URL via URL.createObjectURL(denoisedBlob) that is stored as a timeline element's sourceUrl but is never revoked when the element is removed or the function is re-invoked.

apps/web/src/types/ai.ts

  1. Dead DenoiseResult type — Lines 95–98 define DenoiseResult which has no remaining consumers after denoiseAudio was changed to return Blob. The adjacent DenoiseRequest interface (lines 91–93) is also unused.

Verdict

⚠️Changes requested. The stereo fix and API wiring are solid, but the five low-severity issues — particularly the object URL memory leaks and the getAudioDuration cleanup gap — should be addressed before merging to avoid memory accumulation in long SPA editing sessions.


Review with Vorflux

try {
const blob = await aiClient.denoiseAudio(file, strength);
setResultBlob(blob);
setResultUrl(URL.createObjectURL(blob));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(blob) creates a new blob URL on every enhance call, but the previous blob URL (if any) is never revoked via URL.revokeObjectURL(). In a long SPA editing session, this accumulates memory that's only freed on page navigation.

Fix: Before calling setResultUrl(URL.createObjectURL(blob)), revoke the current resultUrl if it exists:

if(resultUrl)URL.revokeObjectURL(resultUrl);

This requires either adding resultUrl to the useCallback deps (also fixing the stale-closure issue noted on line 28) or using a ref.

const handleAddToTimeline = useCallback(async () => {
if (!resultBlob) return;
const outFile = new File([resultBlob], `enhanced_${Date.now()}.wav`, { type: "audio/wav" });
const audioUrl = URL.createObjectURL(outFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(outFile) creates a blob URL stored as the timeline element's sourceUrl. If the user removes the element from the timeline, this blob URL is never revoked. Consider tracking timeline-inserted blob URLs and revoking them when the corresponding element is deleted.


const handlePickFile = useCallback((f: File) => {
setFile(f);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure + missing revoke:handlePickFile has an empty dependency array [], so it can't access the currentresultUrl to call URL.revokeObjectURL() before nullifying it. When the user picks a new file, the previous enhanced preview's blob URL is abandoned without cleanup.

Fix: Use a useRef<string | null> to track the active blob URL so it's always reachable from this callback regardless of the closure.

constresultUrlRef=useRef<string|null>(null);// In handlePickFile:if(resultUrlRef.current)URL.revokeObjectURL(resultUrlRef.current);resultUrlRef.current=null;setResultUrl(null);// After creating URL in handleEnhance:resultUrlRef.current=url;setResultUrl(url);

className="text-[10px] text-destructive hover:text-destructive/80 shrink-0 ml-2"
onClick={() => {
setFile(null);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak: The Remove button handler sets setResultUrl(null) without first revoking the old blob URL via URL.revokeObjectURL(). Same stale-closure issue as handlePickFile — the inline handler can't access the current resultUrl to revoke it. Use the same ref-based approach suggested for handlePickFile on line 28.

);
}

function getAudioDuration(url: string): Promise<number> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio element leak:getAudioDuration creates a new Audio instance on every call but never releases it. On both loadedmetadata and error events, the audio element and its blob URL resource should be cleaned up:

functiongetAudioDuration(url: string): Promise<number>{returnnewPromise((resolve)=>{constaudio=newAudio(url);constcleanup=()=>{audio.removeEventListener("loadedmetadata",onLoaded);audio.removeEventListener("error",onError);audio.src="";audio.remove();if(url.startsWith("blob:"))URL.revokeObjectURL(url);};constonLoaded=()=>{constd=audio.duration;cleanup();resolve(d);};constonError=()=>{cleanup();resolve(5);};audio.addEventListener("loadedmetadata",onLoaded);audio.addEventListener("error",onError);});}

type: "audio/wav",
});
const denoisedBlob = await aiClient.denoiseAudio(file, strength);
const audioUrl = URL.createObjectURL(denoisedBlob);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(denoisedBlob) creates a blob URL that's stored in the timeline element's sourceUrl but never revoked. If the user removes the audio track or re-runs enhance (which would replace the previous blob URL), memory accumulates.

@vorflux

vorfluxBot commented Aug 27, 2026

Copy link
Copy Markdown

Risk Assessment

This PR adds a new Voice Enhance UI feature across two components and fixes a stereo denoising bug in the Python backend. The diff is focused (359 lines, 5 files), the fix is targeted and correct, and the new UI is additive with no changes to existing code paths except the denoiseAudio return type which had no other callers.

Risk score: 2/10 (threshold: 3) — within the configured risk threshold
Overall risk: Low

Concerns

  1. Object URL memory leaks in both new components (EnhanceView and EnhanceAudioSection) — blob URLs accumulate in long SPA sessions without per-URL cleanup. Low severity but worth fixing before heavy user adoption.
  2. getAudioDuration doesn't release the Audio element or its blob URL after resolving — minor resource leak.
  3. No test coverage for the new components or the denoiseAudio method change.

Verdict

Safe to ship. All findings are low-severity and the core logic (stereo fix, API wiring) is correct. Rollback is trivial — revert the PR with no data or migration impact.

vorflux[bot]
vorfluxBot approved these changes Aug 27, 2026

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — the stereo fix is correct and the voice enhance UI wiring is solid. Five low-severity findings (object URL memory leaks, stale closure, dead type) noted inline but none are blockers. Risk score 2/10, within the configured threshold of 3. ✅ Approved.


Review with Vorflux

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@GinoongFlores
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: wire up voice enhance UI, fix denoise stereo bug by GinoongFlores · Pull Request #9 · Ekaanth/OpenCut-AI · GitHub
Skip to content

feat: wire up voice enhance UI, fix denoise stereo bug - #9

Open
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui
Open

feat: wire up voice enhance UI, fix denoise stereo bug#9
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui

Conversation

@GinoongFlores

Copy link
Copy Markdown

Voice Enhance was backend-only (/api/audio/denoise) with no UI, and had a bug that would've broken it if called anyway.

  • new Enhance tab in the Audio panel: upload a file, denoise it, add to timeline
  • new Voice Enhance section on selected video clips: denoises the clip's own audio in place (mutes original, adds cleaned copy as a new track)
  • fix denoiseAudio() parsing a raw WAV response as JSON
  • fix noisereduce channel-axis mismatch (soundfile gives (frames, channels), noisereduce expects (channels, frames)) causing a 500 on stereo audio

- add Enhance tab (upload+denoise a file) and per-clip Voice Enhance
section (denoise a timeline video clip's own audio in place)
- fix denoiseAudio() parsing raw WAV response as JSON
- fix noisereduce channel-axis mismatch (soundfile gives (frames, channels),
noisereduce expects (channels, frames)) causing 500 on stereo audio

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed — found 5 issues across 3 files. The PR wires up the voice enhance UI in a new EnhanceView tab and a EnhanceAudioSection in video properties, plus fixes a stereo denoising bug in the Python backend. The stereo fix is correct and the denoiseAudio return-type fix from DenoiseResult to Blob resolves a latent broken API call. Architecture conformance is good. All findings are low-severity — three object URL memory leaks, one dead type, and one stale callback closure.

Findings

apps/web/src/components/editor/panels/assets/views/enhance.tsx

  1. Object URL memory leaksURL.createObjectURL() is called on lines 43 and 57 without corresponding URL.revokeObjectURL(). Additionally, handlePickFile (line 28) and the Remove button handler (line 100) nullify resultUrl state without revoking the old blob URL first. Blob URLs accumulate in browser memory until page navigation.

  2. getAudioDuration leaks Audio element — Lines 183–188 create a new Audio element per call but never call remove() or set src = "" after the duration is resolved. The blob URL passed to it is also never revoked.

  3. handlePickFile stale closure — The empty dependency array [] on line 31 means the callback can't access the current resultUrl value to revoke its blob URL. A ref should be used to track the active blob URL.

apps/web/src/components/editor/panels/properties/video-properties.tsx

  1. Object URL memory leak in EnhanceAudioSection — Line 95 creates a blob URL via URL.createObjectURL(denoisedBlob) that is stored as a timeline element's sourceUrl but is never revoked when the element is removed or the function is re-invoked.

apps/web/src/types/ai.ts

  1. Dead DenoiseResult type — Lines 95–98 define DenoiseResult which has no remaining consumers after denoiseAudio was changed to return Blob. The adjacent DenoiseRequest interface (lines 91–93) is also unused.

Verdict

⚠️Changes requested. The stereo fix and API wiring are solid, but the five low-severity issues — particularly the object URL memory leaks and the getAudioDuration cleanup gap — should be addressed before merging to avoid memory accumulation in long SPA editing sessions.


Review with Vorflux

try {
const blob = await aiClient.denoiseAudio(file, strength);
setResultBlob(blob);
setResultUrl(URL.createObjectURL(blob));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(blob) creates a new blob URL on every enhance call, but the previous blob URL (if any) is never revoked via URL.revokeObjectURL(). In a long SPA editing session, this accumulates memory that's only freed on page navigation.

Fix: Before calling setResultUrl(URL.createObjectURL(blob)), revoke the current resultUrl if it exists:

if(resultUrl)URL.revokeObjectURL(resultUrl);

This requires either adding resultUrl to the useCallback deps (also fixing the stale-closure issue noted on line 28) or using a ref.

const handleAddToTimeline = useCallback(async () => {
if (!resultBlob) return;
const outFile = new File([resultBlob], `enhanced_${Date.now()}.wav`, { type: "audio/wav" });
const audioUrl = URL.createObjectURL(outFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(outFile) creates a blob URL stored as the timeline element's sourceUrl. If the user removes the element from the timeline, this blob URL is never revoked. Consider tracking timeline-inserted blob URLs and revoking them when the corresponding element is deleted.


const handlePickFile = useCallback((f: File) => {
setFile(f);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure + missing revoke:handlePickFile has an empty dependency array [], so it can't access the currentresultUrl to call URL.revokeObjectURL() before nullifying it. When the user picks a new file, the previous enhanced preview's blob URL is abandoned without cleanup.

Fix: Use a useRef<string | null> to track the active blob URL so it's always reachable from this callback regardless of the closure.

constresultUrlRef=useRef<string|null>(null);// In handlePickFile:if(resultUrlRef.current)URL.revokeObjectURL(resultUrlRef.current);resultUrlRef.current=null;setResultUrl(null);// After creating URL in handleEnhance:resultUrlRef.current=url;setResultUrl(url);

className="text-[10px] text-destructive hover:text-destructive/80 shrink-0 ml-2"
onClick={() => {
setFile(null);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak: The Remove button handler sets setResultUrl(null) without first revoking the old blob URL via URL.revokeObjectURL(). Same stale-closure issue as handlePickFile — the inline handler can't access the current resultUrl to revoke it. Use the same ref-based approach suggested for handlePickFile on line 28.

);
}

function getAudioDuration(url: string): Promise<number> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio element leak:getAudioDuration creates a new Audio instance on every call but never releases it. On both loadedmetadata and error events, the audio element and its blob URL resource should be cleaned up:

functiongetAudioDuration(url: string): Promise<number>{returnnewPromise((resolve)=>{constaudio=newAudio(url);constcleanup=()=>{audio.removeEventListener("loadedmetadata",onLoaded);audio.removeEventListener("error",onError);audio.src="";audio.remove();if(url.startsWith("blob:"))URL.revokeObjectURL(url);};constonLoaded=()=>{constd=audio.duration;cleanup();resolve(d);};constonError=()=>{cleanup();resolve(5);};audio.addEventListener("loadedmetadata",onLoaded);audio.addEventListener("error",onError);});}

type: "audio/wav",
});
const denoisedBlob = await aiClient.denoiseAudio(file, strength);
const audioUrl = URL.createObjectURL(denoisedBlob);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(denoisedBlob) creates a blob URL that's stored in the timeline element's sourceUrl but never revoked. If the user removes the audio track or re-runs enhance (which would replace the previous blob URL), memory accumulates.

@vorflux

vorfluxBot commented Aug 27, 2026

Copy link
Copy Markdown

Risk Assessment

This PR adds a new Voice Enhance UI feature across two components and fixes a stereo denoising bug in the Python backend. The diff is focused (359 lines, 5 files), the fix is targeted and correct, and the new UI is additive with no changes to existing code paths except the denoiseAudio return type which had no other callers.

Risk score: 2/10 (threshold: 3) — within the configured risk threshold
Overall risk: Low

Concerns

  1. Object URL memory leaks in both new components (EnhanceView and EnhanceAudioSection) — blob URLs accumulate in long SPA sessions without per-URL cleanup. Low severity but worth fixing before heavy user adoption.
  2. getAudioDuration doesn't release the Audio element or its blob URL after resolving — minor resource leak.
  3. No test coverage for the new components or the denoiseAudio method change.

Verdict

Safe to ship. All findings are low-severity and the core logic (stereo fix, API wiring) is correct. Rollback is trivial — revert the PR with no data or migration impact.

vorflux[bot]
vorfluxBot approved these changes Aug 27, 2026

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — the stereo fix is correct and the voice enhance UI wiring is solid. Five low-severity findings (object URL memory leaks, stale closure, dead type) noted inline but none are blockers. Risk score 2/10, within the configured threshold of 3. ✅ Approved.


Review with Vorflux

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@GinoongFlores
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: wire up voice enhance UI, fix denoise stereo bug by GinoongFlores · Pull Request #9 · Ekaanth/OpenCut-AI · GitHub
Skip to content

feat: wire up voice enhance UI, fix denoise stereo bug - #9

Open
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui
Open

feat: wire up voice enhance UI, fix denoise stereo bug#9
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui

Conversation

@GinoongFlores

Copy link
Copy Markdown

Voice Enhance was backend-only (/api/audio/denoise) with no UI, and had a bug that would've broken it if called anyway.

  • new Enhance tab in the Audio panel: upload a file, denoise it, add to timeline
  • new Voice Enhance section on selected video clips: denoises the clip's own audio in place (mutes original, adds cleaned copy as a new track)
  • fix denoiseAudio() parsing a raw WAV response as JSON
  • fix noisereduce channel-axis mismatch (soundfile gives (frames, channels), noisereduce expects (channels, frames)) causing a 500 on stereo audio

- add Enhance tab (upload+denoise a file) and per-clip Voice Enhance
section (denoise a timeline video clip's own audio in place)
- fix denoiseAudio() parsing raw WAV response as JSON
- fix noisereduce channel-axis mismatch (soundfile gives (frames, channels),
noisereduce expects (channels, frames)) causing 500 on stereo audio

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed — found 5 issues across 3 files. The PR wires up the voice enhance UI in a new EnhanceView tab and a EnhanceAudioSection in video properties, plus fixes a stereo denoising bug in the Python backend. The stereo fix is correct and the denoiseAudio return-type fix from DenoiseResult to Blob resolves a latent broken API call. Architecture conformance is good. All findings are low-severity — three object URL memory leaks, one dead type, and one stale callback closure.

Findings

apps/web/src/components/editor/panels/assets/views/enhance.tsx

  1. Object URL memory leaksURL.createObjectURL() is called on lines 43 and 57 without corresponding URL.revokeObjectURL(). Additionally, handlePickFile (line 28) and the Remove button handler (line 100) nullify resultUrl state without revoking the old blob URL first. Blob URLs accumulate in browser memory until page navigation.

  2. getAudioDuration leaks Audio element — Lines 183–188 create a new Audio element per call but never call remove() or set src = "" after the duration is resolved. The blob URL passed to it is also never revoked.

  3. handlePickFile stale closure — The empty dependency array [] on line 31 means the callback can't access the current resultUrl value to revoke its blob URL. A ref should be used to track the active blob URL.

apps/web/src/components/editor/panels/properties/video-properties.tsx

  1. Object URL memory leak in EnhanceAudioSection — Line 95 creates a blob URL via URL.createObjectURL(denoisedBlob) that is stored as a timeline element's sourceUrl but is never revoked when the element is removed or the function is re-invoked.

apps/web/src/types/ai.ts

  1. Dead DenoiseResult type — Lines 95–98 define DenoiseResult which has no remaining consumers after denoiseAudio was changed to return Blob. The adjacent DenoiseRequest interface (lines 91–93) is also unused.

Verdict

⚠️Changes requested. The stereo fix and API wiring are solid, but the five low-severity issues — particularly the object URL memory leaks and the getAudioDuration cleanup gap — should be addressed before merging to avoid memory accumulation in long SPA editing sessions.


Review with Vorflux

try {
const blob = await aiClient.denoiseAudio(file, strength);
setResultBlob(blob);
setResultUrl(URL.createObjectURL(blob));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(blob) creates a new blob URL on every enhance call, but the previous blob URL (if any) is never revoked via URL.revokeObjectURL(). In a long SPA editing session, this accumulates memory that's only freed on page navigation.

Fix: Before calling setResultUrl(URL.createObjectURL(blob)), revoke the current resultUrl if it exists:

if(resultUrl)URL.revokeObjectURL(resultUrl);

This requires either adding resultUrl to the useCallback deps (also fixing the stale-closure issue noted on line 28) or using a ref.

const handleAddToTimeline = useCallback(async () => {
if (!resultBlob) return;
const outFile = new File([resultBlob], `enhanced_${Date.now()}.wav`, { type: "audio/wav" });
const audioUrl = URL.createObjectURL(outFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(outFile) creates a blob URL stored as the timeline element's sourceUrl. If the user removes the element from the timeline, this blob URL is never revoked. Consider tracking timeline-inserted blob URLs and revoking them when the corresponding element is deleted.


const handlePickFile = useCallback((f: File) => {
setFile(f);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure + missing revoke:handlePickFile has an empty dependency array [], so it can't access the currentresultUrl to call URL.revokeObjectURL() before nullifying it. When the user picks a new file, the previous enhanced preview's blob URL is abandoned without cleanup.

Fix: Use a useRef<string | null> to track the active blob URL so it's always reachable from this callback regardless of the closure.

constresultUrlRef=useRef<string|null>(null);// In handlePickFile:if(resultUrlRef.current)URL.revokeObjectURL(resultUrlRef.current);resultUrlRef.current=null;setResultUrl(null);// After creating URL in handleEnhance:resultUrlRef.current=url;setResultUrl(url);

className="text-[10px] text-destructive hover:text-destructive/80 shrink-0 ml-2"
onClick={() => {
setFile(null);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak: The Remove button handler sets setResultUrl(null) without first revoking the old blob URL via URL.revokeObjectURL(). Same stale-closure issue as handlePickFile — the inline handler can't access the current resultUrl to revoke it. Use the same ref-based approach suggested for handlePickFile on line 28.

);
}

function getAudioDuration(url: string): Promise<number> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio element leak:getAudioDuration creates a new Audio instance on every call but never releases it. On both loadedmetadata and error events, the audio element and its blob URL resource should be cleaned up:

functiongetAudioDuration(url: string): Promise<number>{returnnewPromise((resolve)=>{constaudio=newAudio(url);constcleanup=()=>{audio.removeEventListener("loadedmetadata",onLoaded);audio.removeEventListener("error",onError);audio.src="";audio.remove();if(url.startsWith("blob:"))URL.revokeObjectURL(url);};constonLoaded=()=>{constd=audio.duration;cleanup();resolve(d);};constonError=()=>{cleanup();resolve(5);};audio.addEventListener("loadedmetadata",onLoaded);audio.addEventListener("error",onError);});}

type: "audio/wav",
});
const denoisedBlob = await aiClient.denoiseAudio(file, strength);
const audioUrl = URL.createObjectURL(denoisedBlob);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(denoisedBlob) creates a blob URL that's stored in the timeline element's sourceUrl but never revoked. If the user removes the audio track or re-runs enhance (which would replace the previous blob URL), memory accumulates.

@vorflux

vorfluxBot commented Aug 27, 2026

Copy link
Copy Markdown

Risk Assessment

This PR adds a new Voice Enhance UI feature across two components and fixes a stereo denoising bug in the Python backend. The diff is focused (359 lines, 5 files), the fix is targeted and correct, and the new UI is additive with no changes to existing code paths except the denoiseAudio return type which had no other callers.

Risk score: 2/10 (threshold: 3) — within the configured risk threshold
Overall risk: Low

Concerns

  1. Object URL memory leaks in both new components (EnhanceView and EnhanceAudioSection) — blob URLs accumulate in long SPA sessions without per-URL cleanup. Low severity but worth fixing before heavy user adoption.
  2. getAudioDuration doesn't release the Audio element or its blob URL after resolving — minor resource leak.
  3. No test coverage for the new components or the denoiseAudio method change.

Verdict

Safe to ship. All findings are low-severity and the core logic (stereo fix, API wiring) is correct. Rollback is trivial — revert the PR with no data or migration impact.

vorflux[bot]
vorfluxBot approved these changes Aug 27, 2026

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — the stereo fix is correct and the voice enhance UI wiring is solid. Five low-severity findings (object URL memory leaks, stale closure, dead type) noted inline but none are blockers. Risk score 2/10, within the configured threshold of 3. ✅ Approved.


Review with Vorflux

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@GinoongFlores
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: wire up voice enhance UI, fix denoise stereo bug by GinoongFlores · Pull Request #9 · Ekaanth/OpenCut-AI · GitHub
Skip to content

feat: wire up voice enhance UI, fix denoise stereo bug - #9

Open
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui
Open

feat: wire up voice enhance UI, fix denoise stereo bug#9
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui

Conversation

@GinoongFlores

Copy link
Copy Markdown

Voice Enhance was backend-only (/api/audio/denoise) with no UI, and had a bug that would've broken it if called anyway.

  • new Enhance tab in the Audio panel: upload a file, denoise it, add to timeline
  • new Voice Enhance section on selected video clips: denoises the clip's own audio in place (mutes original, adds cleaned copy as a new track)
  • fix denoiseAudio() parsing a raw WAV response as JSON
  • fix noisereduce channel-axis mismatch (soundfile gives (frames, channels), noisereduce expects (channels, frames)) causing a 500 on stereo audio

- add Enhance tab (upload+denoise a file) and per-clip Voice Enhance
section (denoise a timeline video clip's own audio in place)
- fix denoiseAudio() parsing raw WAV response as JSON
- fix noisereduce channel-axis mismatch (soundfile gives (frames, channels),
noisereduce expects (channels, frames)) causing 500 on stereo audio

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed — found 5 issues across 3 files. The PR wires up the voice enhance UI in a new EnhanceView tab and a EnhanceAudioSection in video properties, plus fixes a stereo denoising bug in the Python backend. The stereo fix is correct and the denoiseAudio return-type fix from DenoiseResult to Blob resolves a latent broken API call. Architecture conformance is good. All findings are low-severity — three object URL memory leaks, one dead type, and one stale callback closure.

Findings

apps/web/src/components/editor/panels/assets/views/enhance.tsx

  1. Object URL memory leaksURL.createObjectURL() is called on lines 43 and 57 without corresponding URL.revokeObjectURL(). Additionally, handlePickFile (line 28) and the Remove button handler (line 100) nullify resultUrl state without revoking the old blob URL first. Blob URLs accumulate in browser memory until page navigation.

  2. getAudioDuration leaks Audio element — Lines 183–188 create a new Audio element per call but never call remove() or set src = "" after the duration is resolved. The blob URL passed to it is also never revoked.

  3. handlePickFile stale closure — The empty dependency array [] on line 31 means the callback can't access the current resultUrl value to revoke its blob URL. A ref should be used to track the active blob URL.

apps/web/src/components/editor/panels/properties/video-properties.tsx

  1. Object URL memory leak in EnhanceAudioSection — Line 95 creates a blob URL via URL.createObjectURL(denoisedBlob) that is stored as a timeline element's sourceUrl but is never revoked when the element is removed or the function is re-invoked.

apps/web/src/types/ai.ts

  1. Dead DenoiseResult type — Lines 95–98 define DenoiseResult which has no remaining consumers after denoiseAudio was changed to return Blob. The adjacent DenoiseRequest interface (lines 91–93) is also unused.

Verdict

⚠️Changes requested. The stereo fix and API wiring are solid, but the five low-severity issues — particularly the object URL memory leaks and the getAudioDuration cleanup gap — should be addressed before merging to avoid memory accumulation in long SPA editing sessions.


Review with Vorflux

try {
const blob = await aiClient.denoiseAudio(file, strength);
setResultBlob(blob);
setResultUrl(URL.createObjectURL(blob));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(blob) creates a new blob URL on every enhance call, but the previous blob URL (if any) is never revoked via URL.revokeObjectURL(). In a long SPA editing session, this accumulates memory that's only freed on page navigation.

Fix: Before calling setResultUrl(URL.createObjectURL(blob)), revoke the current resultUrl if it exists:

if(resultUrl)URL.revokeObjectURL(resultUrl);

This requires either adding resultUrl to the useCallback deps (also fixing the stale-closure issue noted on line 28) or using a ref.

const handleAddToTimeline = useCallback(async () => {
if (!resultBlob) return;
const outFile = new File([resultBlob], `enhanced_${Date.now()}.wav`, { type: "audio/wav" });
const audioUrl = URL.createObjectURL(outFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(outFile) creates a blob URL stored as the timeline element's sourceUrl. If the user removes the element from the timeline, this blob URL is never revoked. Consider tracking timeline-inserted blob URLs and revoking them when the corresponding element is deleted.


const handlePickFile = useCallback((f: File) => {
setFile(f);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure + missing revoke:handlePickFile has an empty dependency array [], so it can't access the currentresultUrl to call URL.revokeObjectURL() before nullifying it. When the user picks a new file, the previous enhanced preview's blob URL is abandoned without cleanup.

Fix: Use a useRef<string | null> to track the active blob URL so it's always reachable from this callback regardless of the closure.

constresultUrlRef=useRef<string|null>(null);// In handlePickFile:if(resultUrlRef.current)URL.revokeObjectURL(resultUrlRef.current);resultUrlRef.current=null;setResultUrl(null);// After creating URL in handleEnhance:resultUrlRef.current=url;setResultUrl(url);

className="text-[10px] text-destructive hover:text-destructive/80 shrink-0 ml-2"
onClick={() => {
setFile(null);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak: The Remove button handler sets setResultUrl(null) without first revoking the old blob URL via URL.revokeObjectURL(). Same stale-closure issue as handlePickFile — the inline handler can't access the current resultUrl to revoke it. Use the same ref-based approach suggested for handlePickFile on line 28.

);
}

function getAudioDuration(url: string): Promise<number> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio element leak:getAudioDuration creates a new Audio instance on every call but never releases it. On both loadedmetadata and error events, the audio element and its blob URL resource should be cleaned up:

functiongetAudioDuration(url: string): Promise<number>{returnnewPromise((resolve)=>{constaudio=newAudio(url);constcleanup=()=>{audio.removeEventListener("loadedmetadata",onLoaded);audio.removeEventListener("error",onError);audio.src="";audio.remove();if(url.startsWith("blob:"))URL.revokeObjectURL(url);};constonLoaded=()=>{constd=audio.duration;cleanup();resolve(d);};constonError=()=>{cleanup();resolve(5);};audio.addEventListener("loadedmetadata",onLoaded);audio.addEventListener("error",onError);});}

type: "audio/wav",
});
const denoisedBlob = await aiClient.denoiseAudio(file, strength);
const audioUrl = URL.createObjectURL(denoisedBlob);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(denoisedBlob) creates a blob URL that's stored in the timeline element's sourceUrl but never revoked. If the user removes the audio track or re-runs enhance (which would replace the previous blob URL), memory accumulates.

@vorflux

vorfluxBot commented Aug 27, 2026

Copy link
Copy Markdown

Risk Assessment

This PR adds a new Voice Enhance UI feature across two components and fixes a stereo denoising bug in the Python backend. The diff is focused (359 lines, 5 files), the fix is targeted and correct, and the new UI is additive with no changes to existing code paths except the denoiseAudio return type which had no other callers.

Risk score: 2/10 (threshold: 3) — within the configured risk threshold
Overall risk: Low

Concerns

  1. Object URL memory leaks in both new components (EnhanceView and EnhanceAudioSection) — blob URLs accumulate in long SPA sessions without per-URL cleanup. Low severity but worth fixing before heavy user adoption.
  2. getAudioDuration doesn't release the Audio element or its blob URL after resolving — minor resource leak.
  3. No test coverage for the new components or the denoiseAudio method change.

Verdict

Safe to ship. All findings are low-severity and the core logic (stereo fix, API wiring) is correct. Rollback is trivial — revert the PR with no data or migration impact.

vorflux[bot]
vorfluxBot approved these changes Aug 27, 2026

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — the stereo fix is correct and the voice enhance UI wiring is solid. Five low-severity findings (object URL memory leaks, stale closure, dead type) noted inline but none are blockers. Risk score 2/10, within the configured threshold of 3. ✅ Approved.


Review with Vorflux

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@GinoongFlores
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: wire up voice enhance UI, fix denoise stereo bug by GinoongFlores · Pull Request #9 · Ekaanth/OpenCut-AI · GitHub
Skip to content

feat: wire up voice enhance UI, fix denoise stereo bug - #9

Open
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui
Open

feat: wire up voice enhance UI, fix denoise stereo bug#9
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui

Conversation

@GinoongFlores

Copy link
Copy Markdown

Voice Enhance was backend-only (/api/audio/denoise) with no UI, and had a bug that would've broken it if called anyway.

  • new Enhance tab in the Audio panel: upload a file, denoise it, add to timeline
  • new Voice Enhance section on selected video clips: denoises the clip's own audio in place (mutes original, adds cleaned copy as a new track)
  • fix denoiseAudio() parsing a raw WAV response as JSON
  • fix noisereduce channel-axis mismatch (soundfile gives (frames, channels), noisereduce expects (channels, frames)) causing a 500 on stereo audio

- add Enhance tab (upload+denoise a file) and per-clip Voice Enhance
section (denoise a timeline video clip's own audio in place)
- fix denoiseAudio() parsing raw WAV response as JSON
- fix noisereduce channel-axis mismatch (soundfile gives (frames, channels),
noisereduce expects (channels, frames)) causing 500 on stereo audio

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed — found 5 issues across 3 files. The PR wires up the voice enhance UI in a new EnhanceView tab and a EnhanceAudioSection in video properties, plus fixes a stereo denoising bug in the Python backend. The stereo fix is correct and the denoiseAudio return-type fix from DenoiseResult to Blob resolves a latent broken API call. Architecture conformance is good. All findings are low-severity — three object URL memory leaks, one dead type, and one stale callback closure.

Findings

apps/web/src/components/editor/panels/assets/views/enhance.tsx

  1. Object URL memory leaksURL.createObjectURL() is called on lines 43 and 57 without corresponding URL.revokeObjectURL(). Additionally, handlePickFile (line 28) and the Remove button handler (line 100) nullify resultUrl state without revoking the old blob URL first. Blob URLs accumulate in browser memory until page navigation.

  2. getAudioDuration leaks Audio element — Lines 183–188 create a new Audio element per call but never call remove() or set src = "" after the duration is resolved. The blob URL passed to it is also never revoked.

  3. handlePickFile stale closure — The empty dependency array [] on line 31 means the callback can't access the current resultUrl value to revoke its blob URL. A ref should be used to track the active blob URL.

apps/web/src/components/editor/panels/properties/video-properties.tsx

  1. Object URL memory leak in EnhanceAudioSection — Line 95 creates a blob URL via URL.createObjectURL(denoisedBlob) that is stored as a timeline element's sourceUrl but is never revoked when the element is removed or the function is re-invoked.

apps/web/src/types/ai.ts

  1. Dead DenoiseResult type — Lines 95–98 define DenoiseResult which has no remaining consumers after denoiseAudio was changed to return Blob. The adjacent DenoiseRequest interface (lines 91–93) is also unused.

Verdict

⚠️Changes requested. The stereo fix and API wiring are solid, but the five low-severity issues — particularly the object URL memory leaks and the getAudioDuration cleanup gap — should be addressed before merging to avoid memory accumulation in long SPA editing sessions.


Review with Vorflux

try {
const blob = await aiClient.denoiseAudio(file, strength);
setResultBlob(blob);
setResultUrl(URL.createObjectURL(blob));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(blob) creates a new blob URL on every enhance call, but the previous blob URL (if any) is never revoked via URL.revokeObjectURL(). In a long SPA editing session, this accumulates memory that's only freed on page navigation.

Fix: Before calling setResultUrl(URL.createObjectURL(blob)), revoke the current resultUrl if it exists:

if(resultUrl)URL.revokeObjectURL(resultUrl);

This requires either adding resultUrl to the useCallback deps (also fixing the stale-closure issue noted on line 28) or using a ref.

const handleAddToTimeline = useCallback(async () => {
if (!resultBlob) return;
const outFile = new File([resultBlob], `enhanced_${Date.now()}.wav`, { type: "audio/wav" });
const audioUrl = URL.createObjectURL(outFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(outFile) creates a blob URL stored as the timeline element's sourceUrl. If the user removes the element from the timeline, this blob URL is never revoked. Consider tracking timeline-inserted blob URLs and revoking them when the corresponding element is deleted.


const handlePickFile = useCallback((f: File) => {
setFile(f);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure + missing revoke:handlePickFile has an empty dependency array [], so it can't access the currentresultUrl to call URL.revokeObjectURL() before nullifying it. When the user picks a new file, the previous enhanced preview's blob URL is abandoned without cleanup.

Fix: Use a useRef<string | null> to track the active blob URL so it's always reachable from this callback regardless of the closure.

constresultUrlRef=useRef<string|null>(null);// In handlePickFile:if(resultUrlRef.current)URL.revokeObjectURL(resultUrlRef.current);resultUrlRef.current=null;setResultUrl(null);// After creating URL in handleEnhance:resultUrlRef.current=url;setResultUrl(url);

className="text-[10px] text-destructive hover:text-destructive/80 shrink-0 ml-2"
onClick={() => {
setFile(null);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak: The Remove button handler sets setResultUrl(null) without first revoking the old blob URL via URL.revokeObjectURL(). Same stale-closure issue as handlePickFile — the inline handler can't access the current resultUrl to revoke it. Use the same ref-based approach suggested for handlePickFile on line 28.

);
}

function getAudioDuration(url: string): Promise<number> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio element leak:getAudioDuration creates a new Audio instance on every call but never releases it. On both loadedmetadata and error events, the audio element and its blob URL resource should be cleaned up:

functiongetAudioDuration(url: string): Promise<number>{returnnewPromise((resolve)=>{constaudio=newAudio(url);constcleanup=()=>{audio.removeEventListener("loadedmetadata",onLoaded);audio.removeEventListener("error",onError);audio.src="";audio.remove();if(url.startsWith("blob:"))URL.revokeObjectURL(url);};constonLoaded=()=>{constd=audio.duration;cleanup();resolve(d);};constonError=()=>{cleanup();resolve(5);};audio.addEventListener("loadedmetadata",onLoaded);audio.addEventListener("error",onError);});}

type: "audio/wav",
});
const denoisedBlob = await aiClient.denoiseAudio(file, strength);
const audioUrl = URL.createObjectURL(denoisedBlob);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(denoisedBlob) creates a blob URL that's stored in the timeline element's sourceUrl but never revoked. If the user removes the audio track or re-runs enhance (which would replace the previous blob URL), memory accumulates.

@vorflux

vorfluxBot commented Aug 27, 2026

Copy link
Copy Markdown

Risk Assessment

This PR adds a new Voice Enhance UI feature across two components and fixes a stereo denoising bug in the Python backend. The diff is focused (359 lines, 5 files), the fix is targeted and correct, and the new UI is additive with no changes to existing code paths except the denoiseAudio return type which had no other callers.

Risk score: 2/10 (threshold: 3) — within the configured risk threshold
Overall risk: Low

Concerns

  1. Object URL memory leaks in both new components (EnhanceView and EnhanceAudioSection) — blob URLs accumulate in long SPA sessions without per-URL cleanup. Low severity but worth fixing before heavy user adoption.
  2. getAudioDuration doesn't release the Audio element or its blob URL after resolving — minor resource leak.
  3. No test coverage for the new components or the denoiseAudio method change.

Verdict

Safe to ship. All findings are low-severity and the core logic (stereo fix, API wiring) is correct. Rollback is trivial — revert the PR with no data or migration impact.

vorflux[bot]
vorfluxBot approved these changes Aug 27, 2026

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — the stereo fix is correct and the voice enhance UI wiring is solid. Five low-severity findings (object URL memory leaks, stale closure, dead type) noted inline but none are blockers. Risk score 2/10, within the configured threshold of 3. ✅ Approved.


Review with Vorflux

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@GinoongFlores
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: wire up voice enhance UI, fix denoise stereo bug by GinoongFlores · Pull Request #9 · Ekaanth/OpenCut-AI · GitHub
Skip to content

feat: wire up voice enhance UI, fix denoise stereo bug - #9

Open
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui
Open

feat: wire up voice enhance UI, fix denoise stereo bug#9
GinoongFlores wants to merge 1 commit into
Ekaanth:mainfrom
GinoongFlores:feat/voice-enhance-ui

Conversation

@GinoongFlores

Copy link
Copy Markdown

Voice Enhance was backend-only (/api/audio/denoise) with no UI, and had a bug that would've broken it if called anyway.

  • new Enhance tab in the Audio panel: upload a file, denoise it, add to timeline
  • new Voice Enhance section on selected video clips: denoises the clip's own audio in place (mutes original, adds cleaned copy as a new track)
  • fix denoiseAudio() parsing a raw WAV response as JSON
  • fix noisereduce channel-axis mismatch (soundfile gives (frames, channels), noisereduce expects (channels, frames)) causing a 500 on stereo audio

- add Enhance tab (upload+denoise a file) and per-clip Voice Enhance
section (denoise a timeline video clip's own audio in place)
- fix denoiseAudio() parsing raw WAV response as JSON
- fix noisereduce channel-axis mismatch (soundfile gives (frames, channels),
noisereduce expects (channels, frames)) causing 500 on stereo audio

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed — found 5 issues across 3 files. The PR wires up the voice enhance UI in a new EnhanceView tab and a EnhanceAudioSection in video properties, plus fixes a stereo denoising bug in the Python backend. The stereo fix is correct and the denoiseAudio return-type fix from DenoiseResult to Blob resolves a latent broken API call. Architecture conformance is good. All findings are low-severity — three object URL memory leaks, one dead type, and one stale callback closure.

Findings

apps/web/src/components/editor/panels/assets/views/enhance.tsx

  1. Object URL memory leaksURL.createObjectURL() is called on lines 43 and 57 without corresponding URL.revokeObjectURL(). Additionally, handlePickFile (line 28) and the Remove button handler (line 100) nullify resultUrl state without revoking the old blob URL first. Blob URLs accumulate in browser memory until page navigation.

  2. getAudioDuration leaks Audio element — Lines 183–188 create a new Audio element per call but never call remove() or set src = "" after the duration is resolved. The blob URL passed to it is also never revoked.

  3. handlePickFile stale closure — The empty dependency array [] on line 31 means the callback can't access the current resultUrl value to revoke its blob URL. A ref should be used to track the active blob URL.

apps/web/src/components/editor/panels/properties/video-properties.tsx

  1. Object URL memory leak in EnhanceAudioSection — Line 95 creates a blob URL via URL.createObjectURL(denoisedBlob) that is stored as a timeline element's sourceUrl but is never revoked when the element is removed or the function is re-invoked.

apps/web/src/types/ai.ts

  1. Dead DenoiseResult type — Lines 95–98 define DenoiseResult which has no remaining consumers after denoiseAudio was changed to return Blob. The adjacent DenoiseRequest interface (lines 91–93) is also unused.

Verdict

⚠️Changes requested. The stereo fix and API wiring are solid, but the five low-severity issues — particularly the object URL memory leaks and the getAudioDuration cleanup gap — should be addressed before merging to avoid memory accumulation in long SPA editing sessions.


Review with Vorflux

try {
const blob = await aiClient.denoiseAudio(file, strength);
setResultBlob(blob);
setResultUrl(URL.createObjectURL(blob));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(blob) creates a new blob URL on every enhance call, but the previous blob URL (if any) is never revoked via URL.revokeObjectURL(). In a long SPA editing session, this accumulates memory that's only freed on page navigation.

Fix: Before calling setResultUrl(URL.createObjectURL(blob)), revoke the current resultUrl if it exists:

if(resultUrl)URL.revokeObjectURL(resultUrl);

This requires either adding resultUrl to the useCallback deps (also fixing the stale-closure issue noted on line 28) or using a ref.

const handleAddToTimeline = useCallback(async () => {
if (!resultBlob) return;
const outFile = new File([resultBlob], `enhanced_${Date.now()}.wav`, { type: "audio/wav" });
const audioUrl = URL.createObjectURL(outFile);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(outFile) creates a blob URL stored as the timeline element's sourceUrl. If the user removes the element from the timeline, this blob URL is never revoked. Consider tracking timeline-inserted blob URLs and revoking them when the corresponding element is deleted.


const handlePickFile = useCallback((f: File) => {
setFile(f);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale closure + missing revoke:handlePickFile has an empty dependency array [], so it can't access the currentresultUrl to call URL.revokeObjectURL() before nullifying it. When the user picks a new file, the previous enhanced preview's blob URL is abandoned without cleanup.

Fix: Use a useRef<string | null> to track the active blob URL so it's always reachable from this callback regardless of the closure.

constresultUrlRef=useRef<string|null>(null);// In handlePickFile:if(resultUrlRef.current)URL.revokeObjectURL(resultUrlRef.current);resultUrlRef.current=null;setResultUrl(null);// After creating URL in handleEnhance:resultUrlRef.current=url;setResultUrl(url);

className="text-[10px] text-destructive hover:text-destructive/80 shrink-0 ml-2"
onClick={() => {
setFile(null);
setResultUrl(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak: The Remove button handler sets setResultUrl(null) without first revoking the old blob URL via URL.revokeObjectURL(). Same stale-closure issue as handlePickFile — the inline handler can't access the current resultUrl to revoke it. Use the same ref-based approach suggested for handlePickFile on line 28.

);
}

function getAudioDuration(url: string): Promise<number> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio element leak:getAudioDuration creates a new Audio instance on every call but never releases it. On both loadedmetadata and error events, the audio element and its blob URL resource should be cleaned up:

functiongetAudioDuration(url: string): Promise<number>{returnnewPromise((resolve)=>{constaudio=newAudio(url);constcleanup=()=>{audio.removeEventListener("loadedmetadata",onLoaded);audio.removeEventListener("error",onError);audio.src="";audio.remove();if(url.startsWith("blob:"))URL.revokeObjectURL(url);};constonLoaded=()=>{constd=audio.duration;cleanup();resolve(d);};constonError=()=>{cleanup();resolve(5);};audio.addEventListener("loadedmetadata",onLoaded);audio.addEventListener("error",onError);});}

type: "audio/wav",
});
const denoisedBlob = await aiClient.denoiseAudio(file, strength);
const audioUrl = URL.createObjectURL(denoisedBlob);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object URL memory leak:URL.createObjectURL(denoisedBlob) creates a blob URL that's stored in the timeline element's sourceUrl but never revoked. If the user removes the audio track or re-runs enhance (which would replace the previous blob URL), memory accumulates.

@vorflux

vorfluxBot commented Aug 27, 2026

Copy link
Copy Markdown

Risk Assessment

This PR adds a new Voice Enhance UI feature across two components and fixes a stereo denoising bug in the Python backend. The diff is focused (359 lines, 5 files), the fix is targeted and correct, and the new UI is additive with no changes to existing code paths except the denoiseAudio return type which had no other callers.

Risk score: 2/10 (threshold: 3) — within the configured risk threshold
Overall risk: Low

Concerns

  1. Object URL memory leaks in both new components (EnhanceView and EnhanceAudioSection) — blob URLs accumulate in long SPA sessions without per-URL cleanup. Low severity but worth fixing before heavy user adoption.
  2. getAudioDuration doesn't release the Audio element or its blob URL after resolving — minor resource leak.
  3. No test coverage for the new components or the denoiseAudio method change.

Verdict

Safe to ship. All findings are low-severity and the core logic (stereo fix, API wiring) is correct. Rollback is trivial — revert the PR with no data or migration impact.

vorflux[bot]
vorfluxBot approved these changes Aug 27, 2026

@vorfluxvorfluxBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — the stereo fix is correct and the voice enhance UI wiring is solid. Five low-severity findings (object URL memory leaks, stale closure, dead type) noted inline but none are blockers. Risk score 2/10, within the configured threshold of 3. ✅ Approved.


Review with Vorflux

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@GinoongFlores