diff --git a/aiia_browser_node.py b/aiia_browser_node.py index daacbb2..ead0b64 100755 --- a/aiia_browser_node.py +++ b/aiia_browser_node.py @@ -248,6 +248,35 @@ async def get_video_poster(request): traceback.print_exc() return web.Response(status=500, text=str(e)) +async def delete_item(request): + data = await request.json() + relative_path_str = data.get("path", "") + filename = data.get("filename", "") + if not filename: + return web.Response(status=400, text="Filename is required.") + + try: + file_path = get_safe_path(output_dir, os.path.join(relative_path_str, filename)) + + if file_path.is_file(): + os.remove(file_path) + # Also cleanup potential cache files + cache_thumb = get_safe_path(image_thumb_dir, os.path.join(relative_path_str, f"{file_path.stem}.jpg")) + if cache_thumb.exists(): os.remove(cache_thumb) + cache_poster = get_safe_path(video_poster_dir, os.path.join(relative_path_str, f"{file_path.stem}.jpg")) + if cache_poster.exists(): os.remove(cache_poster) + + return web.json_response({"status": "success", "message": f"File {filename} deleted."}) + elif file_path.is_dir(): + shutil.rmtree(file_path) + return web.json_response({"status": "success", "message": f"Directory {filename} deleted."}) + else: + return web.Response(status=404, text="Item not found") + + except Exception as e: + traceback.print_exc() + return web.Response(status=500, text=str(e)) + async def get_batch_metadata(request): data = await request.json() path = data.get("path", "") @@ -336,6 +365,7 @@ server.PromptServer.instance.app.router.add_post('/api/aiia/v1/browser/get_batch server.PromptServer.instance.app.router.add_get('/api/aiia/v1/browser/thumbnail', get_thumbnail) server.PromptServer.instance.app.router.add_get('/api/aiia/v1/browser/poster', get_video_poster) server.PromptServer.instance.app.router.add_get('/api/aiia/v1/browser/get_workflow', get_workflow) +server.PromptServer.instance.app.router.add_post('/api/aiia/v1/browser/delete_item', delete_item) NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} \ No newline at end of file diff --git a/js/aiia_browser.js b/js/aiia_browser.js index 72ec279..ebca68b 100755 --- a/js/aiia_browser.js +++ b/js/aiia_browser.js @@ -76,7 +76,7 @@ class AIIABrowserDialog extends ComfyDialog { this.iconView = { resizeObserver: null, columns: 0, rowHeight: 0, activeNodes: new Map(), nodePool: [], mediaCache: new Map() }; this.applyFocusRaf = null; this.iconLoadTimers = new Map(); - + this.outsideClickListener = null; this.tooltipImage = $el("img.aiia-tooltip-image"); @@ -300,6 +300,25 @@ class AIIABrowserDialog extends ComfyDialog { } } } + async deleteItem(itemName) { + if (!itemName) return; + if (!confirm(`Are you sure you want to delete "${itemName}"? This action cannot be undone.`)) return; + try { + const res = await api.fetchApi('/aiia/v1/browser/delete_item', { + method: 'POST', + body: JSON.stringify({ path: this.currentPath, filename: itemName }) + }); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`); + const result = await res.json(); + if (result.status === 'success') { + this.setFocus(null, null); + this.refreshCurrentDirectory(); + } + } catch (e) { + alert(`Error deleting item: ${e.message}`); + console.error(e); + } + } updateTooltipSizeLimits() { const rect = this.contentPanel.getBoundingClientRect(); const maxSize = Math.min(rect.width, rect.height) * 0.30; this.tooltipElement.style.setProperty('--aiia-tooltip-media-max-size', `${maxSize}px`); } updateSortControls() { this.sortKeySelect.value = this.sortKey; this.sortDirButton.textContent = this.sortDir === 'asc' ? '▲' : '▼'; this.sortDirButton.dataset.tooltipText = `Sort ${this.sortDir === 'asc' ? 'Descending' : 'Ascending'}`; } showPathInput() { this.breadcrumbs.style.display = 'none'; this.pathInputContainer.style.display = 'flex'; this.pathInput.value = this.currentPath; this.pathInput.focus(); this.pathInput.select(); } @@ -1313,11 +1332,15 @@ class AIIABrowserDialog extends ComfyDialog { if ((target.tagName === 'INPUT' && target !== this.pathInput) || target.tagName === 'SELECT') return; - const keyMap = { 'ArrowUp': 'moveFocus', 'ArrowDown': 'moveFocus', 'ArrowLeft': 'moveFocus', 'ArrowRight': 'moveFocus', 'Enter': 'activateFocusedItem', ' ': 'toggleTooltipForFocusedItem', 'Escape': 'close' }; + const keyMap = { 'ArrowUp': 'moveFocus', 'ArrowDown': 'moveFocus', 'ArrowLeft': 'moveFocus', 'ArrowRight': 'moveFocus', 'Enter': 'activateFocusedItem', ' ': 'toggleTooltipForFocusedItem', 'Escape': 'close', 'Delete': 'deleteFocusedItem' }; if (keyMap[e.key]) { e.preventDefault(); e.stopPropagation(); this.isKeyboardNavigating = true; if (keyMap[e.key] === 'moveFocus') this.moveFocus(e.key); + else if (keyMap[e.key] === 'deleteFocusedItem') { + const item = this.getAllNavigableItems()[this.currentFocusIndex]; + if (item) this.deleteItem(item.name); + } else this[keyMap[e.key]](); } } diff --git a/libs/Ditto/core/atomic_components/audio2motion.py b/libs/Ditto/core/atomic_components/audio2motion.py index 370d843..3fff34f 100644 --- a/libs/Ditto/core/atomic_components/audio2motion.py +++ b/libs/Ditto/core/atomic_components/audio2motion.py @@ -161,7 +161,7 @@ class Audio2Motion: return res_kp_seq - def _update_kp_cond(self, res_kp_seq, idx): + def _update_kp_cond(self, res_kp_seq, idx, step_len=0): # [v1.9.144] Unconditional State Tracking # We process these outside the fix_kp_cond branches to ensure monitoring never stops. last_pose = res_kp_seq[:, idx-1] @@ -175,11 +175,25 @@ class Audio2Motion: self.global_time += 1 delta_p = pitch_deg - self.s_pitch_deg - # [v1.9.149] Pulse Heartbeat (Every Chunk) - # 1:1 log frequency to eliminate blind spots + # [v1.9.150] Detailed Diagnostic Pulse + # Always output current frame pitch and silence status real_f = res_kp_seq.shape[1] tag = "[Postural HEARTBEAT]" if self.look_up_timer <= 50 else "[RECOVERY ACTIVE]" - print(f"{tag} Frame {real_f} | Raw={pitch_deg:.2f}° | Delta={delta_p:+.2f}° | Timer={self.look_up_timer}/50") + print(f"{tag} Frame {real_f:04d} | Delta={delta_p:+.2f}° | Silence={self.silence_frames:03d} | Timer={self.look_up_timer}/50") + + # 5. Postural Auto-Correction Logic [v1.9.150 Robustness Patch] + # Shorten silence threshold to 25 frames (1 second) to combat jittery audio + # Only trigger if notably higher than source (> 2.0 deg Higher -> Delta < -2.0) + if self.silence_frames >= 25 and delta_p < -2.0: + self.look_up_timer += step_len + if self.look_up_timer > 50: + print(f"[正在执行纠偏] Frame {real_f} | Delta={delta_p:+.2f}° | Active Down-Pull engaged.") + else: + # ONLY RESET if we are back in the safe zone (Hysteresis) + if delta_p >= -0.5: + if self.look_up_timer > 50: + print(f"[Postural] Recovery Finished (Delta={delta_p:+.2f}°). Timer reset.") + self.look_up_timer = 0 if self.fix_kp_cond == 0: # 不重置 # 1. Silence Intensity Boost @@ -213,23 +227,10 @@ class Audio2Motion: brow_indices = [217, 218, 220] brow_jitter = np.random.normal(0, 0.015 * boost_factor, len(brow_indices)).astype(np.float32) - # 5. Postural Auto-Correction (Anti-Stall) - [v1.9.149 Frame-Sync Fix] - if self.silence_frames > 50 and delta_p < -2.0: - # v1.149: Increment by frames added, not just 1. - self.look_up_timer += self.valid_clip_len - if self.look_up_timer > 50: - diag_tag = "[正在执行纠偏]" - print(f"{diag_tag} Frame {real_f} | Delta={delta_p:+.2f}° | Push-Down Active") - else: - # Reset only if significantly recovered (Hysteresis) - if delta_p > -0.5: - if self.look_up_timer > 50: - print(f"[Postural] Recovery Finished at Delta={delta_p:+.2f}°. Timer Reset.") - self.look_up_timer = 0 - gravity_vec = np.ones_like(last_pose) * 0.05 if self.look_up_timer > 50: gravity_vec[0, 1:67] = 0.20 + # 6. Integration next_pose = last_pose + noise for i, idx_in_kp in enumerate(brow_indices): @@ -342,7 +343,7 @@ class Audio2Motion: self.clip_idx += 1 idx = res_kp_seq.shape[1] - self.overlap_v2 - self._update_kp_cond(res_kp_seq, idx) + self._update_kp_cond(res_kp_seq, idx, step_len=step_len) return res_kp_seq diff --git a/pyproject.toml b/pyproject.toml index dabdacc..e57034e 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "aiia" description = "The Ultimate AI Audio/Video toolkit for ComfyUI. Features an enhanced Ditto (with optimizations that outperform official demos and other SOTA talking head models in lip-sync accuracy and natural motion), EchoMimic V3 & FLOAT, VibeVoice & CosyVoice 3.0 (Zero-Shot Voice Cloning), Multi-Role Podcast Generation, and a powerful Media Browser." -version = "1.9.149" +version = "1.9.151" license = {file = "LICENSE"} readme = "README.md" authors = [