DOGMA 1.0.41: Fix media preview paths and free edit intervals [skip ci]

This commit is contained in:
axior
2026-11-12 18:51:49 +01:00
parent a3fda9f153
commit 7b4b2ebe65
5 changed files with 45 additions and 34 deletions
+3
View File
@@ -0,0 +1,3 @@
DOGMA Nodes 1.0.41
Fix previews of uploaded media in subfolders. Allow manually entered edit times with comma or dot decimals. Select intervals down to one source frame; pad H3 temporal inputs and trim the generated tail during composition.
+18 -15
View File
@@ -11,9 +11,9 @@ import tempfile
import time
from fractions import Fraction
# Official output range 4–15 s, intersected with Comfy's 17k+5 grid.
# Native H3 temporal grid; the selected interval is independent of this grid.
# 362/24 exceeds the 15 s reference budget; keep the source reference <=15 s.
EDIT_FRAMES = tuple(range(107, 346, 17))
EDIT_FRAMES = tuple(range(5, 346, 17))
def edit_defaults():
@@ -76,18 +76,19 @@ def plan_interval(meta, start, end):
if not all(math.isfinite(v) for v in (start, end)):
raise ValueError('Selezione temporale non valida.')
fps = Fraction(meta['fps_ratio'])
# Same grid used by the mouse handles. Frames are used for the visible
# selection; inference has at most one source-frame of extra tail, cropped.
start_frame = round(start * float(fps))
end_frame = round(end * float(fps))
count = end_frame-start_frame
choices = {int(Fraction(n,24)*fps): n for n in EDIT_FRAMES}
if count not in choices:
raise ValueError('La durata non è sulla griglia H3: regola le maniglie nella timeline.')
start, end = start_frame/float(fps), end_frame/float(fps)
if count < 1:
raise ValueError('Seleziona almeno un fotogramma.')
if start < 0 or end > meta['duration'] + 1e-6:
raise ValueError('La selezione supera il video sorgente.')
return dict(start=start, end=end, duration=end-start, generation_frames=choices[count],
required = math.ceil(count / float(fps) * 24 - 1e-8)
generation_frames = next((n for n in EDIT_FRAMES if n >= required), None)
if generation_frames is None:
raise ValueError('La selezione supera la durata disponibile per H3.')
return dict(start=start, end=end, duration=end-start, generation_frames=generation_frames,
source_frames=count, start_frame=start_frame, end_frame=end_frame)
@@ -108,7 +109,7 @@ def check_config(c, input_file):
videos = [r for r in c['refs'] if r.get('kind') == 'video']
if len(videos)>2 or len(c['refs'])>11:
raise ValueError('Il sorgente occupa Video 1: restano 2 video e 9 immagini reference (11 file).')
duration = plan['generation_frames']/24
reference_duration = 0.
for r in c['refs']:
if r.get('kind') not in ('image','video') or not r.get('name'):
raise ValueError('Carica o rimuovi le reference vuote.')
@@ -117,16 +118,16 @@ def check_config(c, input_file):
ref = probe(path)
if not 2 <= ref['duration'] <= 15:
raise ValueError('Ogni video reference aggiuntivo deve durare da 2 a 15 secondi.')
if max(5,math.floor((ref['duration']*24-5)/17)*17+5)>plan['generation_frames']:
raise ValueError('Un video reference è più lungo del tratto editato: accorcialo prima di caricarlo.')
duration += ref['duration']
plan['generation_frames'] = max(plan['generation_frames'], max(5,math.floor((ref['duration']*24-5)/17)*17+5))
reference_duration += ref['duration']
duration = plan['generation_frames']/24 + reference_duration
if duration>15+1e-6:
raise ValueError(f'Sorgente selezionato + video reference: {duration:.2f} s. Il limite H3 è 15 s totali; accorcia la selezione o le reference.')
ffmpeg() # Fail before loading models.
return meta,plan
def segment_tensor(path, start, n, width, height, interrupt=lambda:None):
def segment_tensor(path, start, n, width, height, interrupt=lambda:None, end=None, source_fps=24):
"""Seek near the selection; sample by timestamps, with no temporal stretch."""
import av
import numpy as np
@@ -135,7 +136,8 @@ def segment_tensor(path, start, n, width, height, interrupt=lambda:None):
stream = container.streams.video[0]
origin = float(stream.start_time*stream.time_base) if stream.start_time is not None else 0.
container.seek(int((origin+start)/float(stream.time_base)),stream=stream,backward=True)
targets = [origin+start+i/24 for i in range(n)]
last = max(start, end-1/source_fps) if end is not None else float('inf')
targets = [origin+min(start+i/24,last) for i in range(n)]
result = torch.empty((n,height,width,3),dtype=torch.float32)
prev = None; pos = 0
def put(frame):
@@ -247,7 +249,8 @@ def run_edit(c,root,st,update,progress,uid):
width=math.ceil(meta['width']/32)*32; height=math.ceil(meta['height']/32)*32
n=plan['generation_frames']
update('running','Lettura della sola selezione…',active='edit')
selected=segment_tensor(source,plan['start'],n,width,height,mm.throw_exception_if_processing_interrupted)
selected=segment_tensor(source,plan['start'],n,width,height,mm.throw_exception_if_processing_interrupted,
end=plan['end'],source_fps=meta['fps'])
images={};videos={'ref_video_1':selected}
for r in c['refs']:
if r['kind']=='image':images[f'ref_image_{len(images)+1}']=director.load_image(r['name'])
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-dogma-nodes"
description = "Custom ComfyUI nodes for DOGMA AI video workflows."
version = "1.0.40"
version = "1.0.41"
license = { file = "LICENSE" }
dependencies = ["scipy>=1.10"]
+6 -3
View File
@@ -1,13 +1,16 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { renderEditor, renderEditResults } from './dogma_h3_video_edit.js?v=video-edit-1';
import { renderEditor, renderEditResults } from './dogma_h3_video_edit.js?v=preview-timing-2';
const ROOT=new URL('.',import.meta.url);
const css=document.createElement('link');css.rel='stylesheet';css.href=new URL('dogma_h3_director_v134.css?v=video-edit-1',ROOT);document.head.append(css);
const css=document.createElement('link');css.rel='stylesheet';css.href=new URL('dogma_h3_director_v134.css?v=preview-timing-2',ROOT);document.head.append(css);
const clone=x=>JSON.parse(JSON.stringify(x));
const el=(tag,text='',cls='')=>{const e=document.createElement(tag);e.textContent=text;e.className=cls;return e;};
const button=(text,fn,cls='')=>{const b=el('button',text,cls);b.type='button';b.onclick=fn;return b;};
const inputURL=name=>api.apiURL('/view?'+new URLSearchParams({filename:name,type:'input'}));
const inputURL=name=>{
const path=name.replaceAll('\\','/'),split=path.lastIndexOf('/');
return api.apiURL('/view?'+new URLSearchParams({filename:path.slice(split+1),subfolder:split<0?'':path.slice(0,split),type:'input'}));
};
const viewURL=file=>api.apiURL('/view?'+new URLSearchParams(file));
const film=file=>{const v=el('video');v.controls=true;v.loop=true;v.playsInline=true;v.preload='metadata';v.src=viewURL(file);return v;};
let active=null;
+17 -15
View File
@@ -4,13 +4,12 @@ const el=(tag,text='',cls='')=>{const e=document.createElement(tag);e.textConten
const button=(text,fn)=>{const e=el('button',text);e.type='button';e.onclick=fn;return e;};
const url=(path,params)=>api.apiURL(path+'?'+new URLSearchParams(params));
const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));
export const editCounts=(fps,frames=Array.from({length:15},(_,i)=>107+17*i))=>frames.map(n=>Math.floor(n*fps/24+1e-8));
export function snapSelection(start,end,meta,side='end'){
const fps=meta.fps,total=Math.floor(meta.duration*fps+1e-6),counts=editCounts(fps,meta.generation_frames).filter(n=>n<=total);
if(!counts.length)throw new Error('Nessuna durata H3 disponibile: serve un sorgente di almeno circa 4,46 s e un totale video reference entro 15 s.');
let a=Math.round(start*fps),b=Math.round(end*fps);
const wanted=Math.max(0,b-a),n=counts.reduce((best,x)=>Math.abs(x-wanted)<Math.abs(best-wanted)?x:best,counts[0]);
if(side==='start'){b=clamp(b,n,total);a=b-n;}else{a=clamp(a,0,total-n);b=a+n;}
const fps=meta.fps,total=Math.floor(meta.duration*fps+1e-6);
const maxCount=Math.min(total,Math.floor(meta.generation_frames.at(-1)*fps/24+1e-8));
let a=clamp(Math.round(start*fps),0,total-1),b=clamp(Math.round(end*fps),1,total);
if(side==='start')a=clamp(a,Math.max(0,b-maxCount),b-1);
else{b=clamp(b,a+1,Math.min(total,a+maxCount));}
return {start:a/fps,end:b/fps};
}
export const clock=t=>{const m=Math.floor(t/60),s=t-m*60;return `${String(m).padStart(2,'0')}:${s.toFixed(3).padStart(6,'0')}`;};
@@ -88,15 +87,17 @@ class EditTimeline{
const others=this.studio.config.refs.filter(r=>r.kind==='video'&&r.name);
const refs=others.length?await Promise.all(others.map(r=>this.studio.request('/dogma/h3/v2/edit/metadata?'+new URLSearchParams({name:r.name})))):[];
if(refs.some(r=>r.duration<2||r.duration>15))throw new Error('Ogni video reference aggiuntivo deve durare da 2 a 15 secondi.');
const budget=15-refs.reduce((sum,r)=>sum+r.duration,0),minimum=Math.max(107,...refs.map(r=>Math.floor((r.duration*24-5)/17)*17+5));
const m={...base,generation_frames:Array.from({length:15},(_,i)=>107+17*i).filter(n=>n>=minimum&&n/24<=budget+1e-8)};
const budget=15-refs.reduce((sum,r)=>sum+r.duration,0),minimum=Math.max(5,...refs.map(r=>Math.floor((r.duration*24-5)/17)*17+5));
const m={...base,generation_frames:Array.from({length:21},(_,i)=>5+17*i).filter(n=>n>=minimum&&n/24<=budget+1e-8)};
if(this.closed)return;
if(!m.generation_frames.length)throw new Error('Le reference video non lasciano spazio per la selezione: accorciale o rimuovile.');
this.meta=m;this.studio.editMeta={...base,source:this.config.source};
Object.assign(this.config,snapSelection(this.config.start,this.config.end,m));this.studio.save();
this.viewStart=0;this.span=Math.min(m.duration,Math.max(20,this.config.end-this.config.start+6));
this.viewStart=clamp(this.config.start-2,0,m.duration-this.span);
const v=this.video=el('video','','dg-edit-player');v.controls=true;v.preload='metadata';v.playsInline=true;
v.src=url('/view',{filename:this.config.source,type:'input'});this.root.append(v);
const path=this.config.source.replaceAll('\\','/'),split=path.lastIndexOf('/');
v.src=url('/view',{filename:path.slice(split+1),subfolder:split<0?'':path.slice(0,split),type:'input'});this.root.append(v);
v.addEventListener('error',()=>this.info.textContent='Il browser non riproduce questo codec. Le miniature restano disponibili; per l’anteprima live usa un MP4 H.264.');
this.info=el('p');this.root.append(this.info);
const controls=el('div','','dg-edit-controls');
@@ -117,7 +118,7 @@ class EditTimeline{
h.className='dg-edit-handle';h.setAttribute('role','slider');h.setAttribute('aria-label',label);h.title=label;
h.addEventListener('pointerdown',e=>this.drag(e,side),{signal:this.abort.signal});
h.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight'].includes(event.key))return;event.preventDefault();
const delta=(event.key==='ArrowLeft'?-1:1)*17/24;this.select(side==='start'?this.config.start+delta:this.config.start,side==='end'?this.config.end+delta:this.config.end,side);});
const delta=(event.key==='ArrowLeft'?-1:1)/m.fps;this.select(side==='start'?this.config.start+delta:this.config.start,side==='end'?this.config.end+delta:this.config.end,side);});
track.append(h);
}
this.band.addEventListener('pointerdown',e=>this.drag(e,'move'),{signal:this.abort.signal});
@@ -128,13 +129,14 @@ class EditTimeline{
this.startInput=studioField('Da · secondi','start');this.endInput=studioField('A · secondi','end');
function studioField(label,key){const input=timelinePlaceholder();return input;
function timelinePlaceholder(){
const wrap=el('label',label,'dg-field'),i=el('input');i.type='number';i.min=0;i.max=m.duration;i.step=1/m.fps;wrap.append(i);times.append(wrap);
i.onchange=()=>{const x=Number(i.value);if(!Number.isFinite(x))return;const c=config;
const wrap=el('label',label,'dg-field'),i=el('input');i.type='text';i.inputMode='decimal';wrap.append(i);times.append(wrap);
i.onchange=()=>{const value=i.value.trim().replace(',','.');const x=Number(value);if(!value||!Number.isFinite(x)){i.setCustomValidity('Inserisci un tempo in secondi.');i.reportValidity();return;}i.setCustomValidity('');const c=config;
Object.assign(c,snapSelection(key==='start'?x:c.start,key==='end'?x:c.end,m,key));
studio.save();studio.editTimeline.paint(false);studio.editTimeline.seek(key==='start'?c.start:Math.max(c.start,c.end-1/m.fps));};return i;
studio.save();studio.editTimeline.paint(false);studio.editTimeline.seek(key==='start'?c.start:Math.max(c.start,c.end-1/m.fps));};
i.oninput=()=>i.setCustomValidity('');i.onkeydown=event=>{if(event.key==='Enter'){event.preventDefault();i.blur();}};return i;
}
}
this.root.append(el('small',`Trascina i bordi arancioni per cambiare la durata; trascina il centro per spostare l’intervallo. Usa la barra del video completo e “Inizia la selezione da qui” per raggiungere scene lontane. Durate H3 disponibili con queste reference: ${(m.generation_frames[0]/24).toFixed(2)}–${(m.generation_frames.at(-1)/24).toFixed(2)} s, a scatti. H3 genera a 24 fps: l’export mantiene i timestamp del sorgente senza interpolazione.`));
this.root.append(el('small',`Trascina i bordi o inserisci i secondi, anche con la virgola. Selezione libera fino a ${(m.generation_frames.at(-1)/24).toFixed(2)} s, con precisione di un fotogramma e senza minimo di 4 secondi. H3 calcola i fotogrammi aggiuntivi necessari; nell’export viene sostituito solo l’intervallo scelto.`));
v.addEventListener('timeupdate',()=>{
if(this.playingSelection&&v.currentTime>=this.config.end){v.pause();this.playingSelection=false;}
this.playhead.style.left=clamp((v.currentTime-this.viewStart)/this.span*100,0,100)+'%';
@@ -149,7 +151,7 @@ class EditTimeline{
select(a,b,side){Object.assign(this.config,snapSelection(a,b,this.meta,side));this.studio.save();this.paint(false);this.seek(side==='start'?this.config.start:this.config.end-1/this.meta.fps);}
drag(event,side){event.preventDefault();event.stopPropagation();const target=event.currentTarget;target.setPointerCapture(event.pointerId);
const initial={a:this.config.start,b:this.config.end,t:this.timeAt(event.clientX)};
const move=ev=>{if(side==='move'){const delta=this.timeAt(ev.clientX)-initial.t;this.select(initial.a+delta,initial.b+delta,'end');}
const move=ev=>{if(side==='move'){const delta=clamp(this.timeAt(ev.clientX)-initial.t,-initial.a,this.meta.duration-initial.b);this.select(initial.a+delta,initial.b+delta,'end');}
else this.select(side==='start'?this.timeAt(ev.clientX):this.config.start,side==='end'?this.timeAt(ev.clientX):this.config.end,side);};
const end=()=>{target.removeEventListener('pointermove',move);target.removeEventListener('pointerup',end);target.removeEventListener('pointercancel',end);};
target.addEventListener('pointermove',move);target.addEventListener('pointerup',end);target.addEventListener('pointercancel',end);