v1.9.151: Postural Recovery Hysteresis Fix & Browser Delete Confirmation - Added delete_item API and keyboard bindings

This commit is contained in:
Hawk Lee
2026-01-24 17:57:51 +08:00
parent 248ff27fa4
commit 6c8cfbdddc
4 changed files with 76 additions and 22 deletions
+30
View File
@@ -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 = {}
+25 -2
View File
@@ -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]]();
}
}
@@ -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
+1 -1
View File
@@ -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 = [