DOGMA 1.0.41: Fix media preview paths and free edit intervals [skip ci]
This commit is contained in:
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user