Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42a976ae63 | ||
|
|
9d3241a272 |
@@ -0,0 +1,10 @@
|
||||
DOGMA Nodes 1.0.50
|
||||
|
||||
- One workflow and one node package for desktop/local and remote/cloud ComfyUI. No SimplePod-only fork, model sampling changes, model downloads or new runtime dependencies.
|
||||
- Fix the local-only 403 on restitch preview, detail, job polling and apply. The loaded H3 catalog supplies an ephemeral browser proof, sent only in a custom header. Require it with JSON for state changes, reject cross-site/opaque requests, do not trust forwarded headers for privileges, do not put the proof into URLs or saved workflows. The catalog response is non-cacheable. This complements existing Comfy/proxy authentication; it is not a new public-server authentication system.
|
||||
- Distinguish localhost desktop requests from public Host/proxy requests at runtime. Native Windows folder opening remains localhost-only. On remote or Linux servers the same Open saved folder button shows an internal browser rooted in the actual Comfy output directory with downloadable media files.
|
||||
- Preserve existing desktop folder selection including drives and paths outside Comfy output. Remote folder selection starts in the server output directory, remembers selections within it and allows subfolders. Remote copies are bounded to output and cannot navigate through symlinks outside it. Sessions and other non-media files are not listed in the media browser.
|
||||
- When importing desktop settings on a POSIX server, replace Windows output paths with the actual server output and let final-specific paths inherit it. Do not alter normal local paths or require a separate JSON. Existing sessions, saved videos and current workflow can continue after node update and page reload.
|
||||
- Missing mask dependency now names the Manager package MaskVidExperiments as well as MVEx_MaskToLatentSpace. Respect HTML hidden controls in action styles, including view-only folder panels.
|
||||
|
||||
Validation: local and proxy/remote tests against production route registration and actual 97-frame CPU restitch, including preview/poll/apply, final copy, output-boundary rejection, stale/missing proof, request origin checks and local Windows OS-call dispatch (mocked to avoid opening Explorer). The same frontend was exercised in an isolated browser with a rewritten internal Host and non-loopback client address, then in localhost mode: server folder selection and media listing, all seven restitch previews without 403, Windows path adaptation and retained local drives/folder navigation. No real SimplePod instance or live local installation was modified during verification. No new H3 sampling was required or run.
|
||||
@@ -0,0 +1,10 @@
|
||||
DOGMA Nodes 1.0.51
|
||||
|
||||
- Integrate HDR finishing into the existing comfyui-dogma-nodes Manager package. No separate node pack, installer, Torch/CUDA update or model download.
|
||||
- Add DOGMAHDRColorChunkedV1 for per-frame SDR-to-ACEScct conversion using the native Comfy colour transform and fp32 intermediates.
|
||||
- Add DOGMAHDRSaveStreamingV1: consume raw decoded ACEScct one frame at a time, restore source proportions in UHD, save ACEScg RGB HALF EXRs with ZIP16 and AP1/D60 chromaticities, and optionally write ProRes 4444 HLG with original PCM24 audio. Preserve original frame count and rational frame rate; omit only LTX temporal padding. Return paths rather than additional complete IMAGE batches. Unique output folders, completion manifest and atomic final MOV name.
|
||||
- Add DOGMAHDRCheckpointV1 before decode; its lazy recovery branch loads a saved latent without running the samplers again. Recovery requires the same source video and compatible VAE.
|
||||
- Preserve previous node IDs, modules, frontend and H3 workflows. HDR registration is optional on older Comfy versions: failure to import the new HDR module does not remove existing DOGMA nodes. HDR requires ComfyUI >=0.45.0; no new declared runtime dependency.
|
||||
- Updated HDR workflow keeps the existing BF16 model, HDR LoRA and scene embedding, fp32 VAE, tile settings and two-stage 8+3 sampling.
|
||||
|
||||
Validation: graph consistency, native colour equivalence, actual HALF EXR round-trip including negative/extended values, ACES metadata, actual ProRes HLG and PCM24 audio, exact rational timestamps, trim/padding, latent save/recovery, unique folders and output boundaries. The unchanged finishing function was additionally exercised on a synthetic shared-frame UHD 144-frame/6-second export, with 0.43 GiB additional peak process RAM. This measurement isolates finishing and is not a full LTX inference/decode or a SimplePod run. Live Comfy installations were not modified.
|
||||
@@ -147,3 +147,22 @@ comfy node install comfyui-dogma-nodes
|
||||
## Notes
|
||||
|
||||
The nodes use only PyTorch and ComfyUI's built-in sampler APIs. No extra Python dependencies are required.
|
||||
|
||||
|
||||
## HDR finishing (DOGMA Nodes 1.0.51)
|
||||
|
||||
The HDR master workflow uses the same DOGMA Nodes Manager package on Windows and Linux.
|
||||
`DOGMAHDRColorChunkedV1`, `DOGMAHDRCheckpointV1`, and `DOGMAHDRSaveStreamingV1`
|
||||
require ComfyUI 0.45.0 or later. Update the existing package; no separate HDR pack
|
||||
or installer is required. Existing H3 workflows and node identifiers are unchanged.
|
||||
|
||||
The master is a 3840x2160 ACEScg RGB HALF EXR sequence (ZIP lossless, AP1/D60).
|
||||
The optional MOV is ProRes 4444 HLG / BT.2020 with 10-bit 4:4:4 encoder input
|
||||
and original PCM24 audio; the EXR sequence is the 16-bit floating-point master.
|
||||
Finishing is sequential per frame, but native VAE decode still returns a complete batch.
|
||||
Output is under the actual Comfy output folder in `DOGMA_HDR_4K/master_XXXXX/`.
|
||||
|
||||
The checkpoint is saved before decode in `DOGMA_HDR_4K/checkpoints/`.
|
||||
To recover an interrupted export, keep the same source loaded and enter the saved
|
||||
`.latent` path (relative to Comfy output) in `resume_from`; leave it empty for a new generation.
|
||||
Do not use `--fp16-intermediates` for the HDR master path.
|
||||
|
||||
+12
@@ -194,3 +194,15 @@ try:
|
||||
del _H3_134, _H3_134_NAMES
|
||||
except Exception as _h3_134_error:
|
||||
print("[DOGMA H3 Director V2] Unavailable; existing nodes preserved:", _h3_134_error)
|
||||
|
||||
|
||||
# Optional HDR finishing. Older Comfy versions retain all previous DOGMA nodes.
|
||||
try:
|
||||
from .dogma_hdr_streaming import NODE_CLASS_MAPPINGS as _HDR151, NODE_DISPLAY_NAME_MAPPINGS as _HDR151_NAMES
|
||||
if set(_HDR151).intersection(NODE_CLASS_MAPPINGS):
|
||||
raise RuntimeError("DOGMA HDR: node ID collision")
|
||||
NODE_CLASS_MAPPINGS.update(_HDR151)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(_HDR151_NAMES)
|
||||
del _HDR151, _HDR151_NAMES
|
||||
except Exception as _hdr151_error:
|
||||
print("[DOGMA HDR] HDR nodes unavailable (requires ComfyUI >= 0.45.0); previous DOGMA nodes preserved:", _hdr151_error)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Browser request proof for H3 data operations; OS actions remain local-only.
|
||||
|
||||
The token complements the deployment's existing Comfy/proxy authentication.
|
||||
It is not a replacement for authentication on a publicly exposed Comfy server.
|
||||
Forwarded headers never grant access or widen the filesystem scope.
|
||||
"""
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit
|
||||
from aiohttp import web
|
||||
|
||||
class H3Access:
|
||||
def __init__(self):self.token=secrets.token_urlsafe(32)
|
||||
|
||||
@staticmethod
|
||||
def fetch_site(request):
|
||||
site=request.headers.get('Sec-Fetch-Site','').lower()
|
||||
if site and site not in ('same-origin','none'):
|
||||
raise web.HTTPForbidden(text='Richiesta da un altro sito non consentita.')
|
||||
origin=request.headers.get('Origin')
|
||||
if origin:
|
||||
try:valid=origin!='null' and urlsplit(origin).scheme in ('http','https') and bool(urlsplit(origin).netloc)
|
||||
except ValueError:valid=False
|
||||
if not valid:raise web.HTTPForbidden(text='Origine non valida.')
|
||||
|
||||
def is_local(self,request):
|
||||
if request.remote not in ('127.0.0.1','::1'):return False
|
||||
try:
|
||||
host=urlsplit('//'+request.host)
|
||||
if host.hostname not in ('127.0.0.1','::1','localhost'):return False
|
||||
origin=request.headers.get('Origin')
|
||||
if origin and urlsplit(origin).netloc.lower()!=host.netloc.lower():return False
|
||||
self.fetch_site(request)
|
||||
except (ValueError,web.HTTPForbidden):return False
|
||||
return True
|
||||
|
||||
def local(self,request):
|
||||
if not self.is_local(request):
|
||||
raise web.HTTPForbidden(text='Questa azione del sistema è disponibile solo da localhost sul computer Comfy.')
|
||||
|
||||
def browser(self,request):
|
||||
self.fetch_site(request)
|
||||
token=request.headers.get('X-Dogma-Access','')
|
||||
if not token or not secrets.compare_digest(token.encode('utf8'),self.token.encode('ascii')):
|
||||
raise web.HTTPForbidden(text='Accesso DOGMA scaduto o pannello precedente. Ricarica la pagina e riapri H3 Director.')
|
||||
if request.method=='POST' and request.content_type!='application/json':
|
||||
raise web.HTTPUnsupportedMediaType(text='Richiesta JSON necessaria.')
|
||||
|
||||
def catalog(self,request,output):
|
||||
self.fetch_site(request)
|
||||
import os
|
||||
return {'token':self.token,'local_files':self.is_local(request),
|
||||
'open_folder':self.is_local(request) and os.name=='nt',
|
||||
'output_directory':str(Path(output).resolve()),'platform':os.name}
|
||||
|
||||
def output_path(self,request,path,root):
|
||||
root=Path(root).expanduser().resolve()
|
||||
destination=Path(path).expanduser().resolve() if path else root
|
||||
if not self.is_local(request) and not destination.is_relative_to(root):
|
||||
raise ValueError('Da remoto scegli una cartella dentro l’output di Comfy: '+str(root))
|
||||
return destination
|
||||
+31
-19
@@ -612,17 +612,13 @@ def register_routes():
|
||||
from .dogma_h3_video_edit import register_edit_routes
|
||||
cleanup=register_edit_routes(routes,input_file)
|
||||
PromptServer.instance.app.on_cleanup.append(cleanup)
|
||||
def local_request(request):
|
||||
from urllib.parse import urlparse
|
||||
if request.remote not in ('127.0.0.1','::1'):
|
||||
raise web.HTTPForbidden(text='Disponibile solo sul computer Comfy.')
|
||||
origin=request.headers.get('Origin')
|
||||
if origin and urlparse(origin).netloc!=request.host:
|
||||
raise web.HTTPForbidden(text='Origine non valida.')
|
||||
from .dogma_h3_access import H3Access
|
||||
access=H3Access()
|
||||
local_request=access.local
|
||||
|
||||
import sys
|
||||
from .dogma_h3_restitch import register_routes as register_restitch
|
||||
PromptServer.instance.app.on_cleanup.append(register_restitch(routes,sys.modules[__name__],local_request))
|
||||
PromptServer.instance.app.on_cleanup.append(register_restitch(routes,sys.modules[__name__],access.browser))
|
||||
|
||||
@routes.post('/dogma/h3/v2/open-folder')
|
||||
async def open_folder(request):
|
||||
@@ -639,10 +635,12 @@ def register_routes():
|
||||
|
||||
@routes.post('/dogma/h3/v2/edit/export-copy')
|
||||
async def export_edit_copy(request):
|
||||
local_request(request)
|
||||
access.browser(request)
|
||||
import asyncio
|
||||
data=await request.json()
|
||||
try:return web.json_response(await asyncio.to_thread(export_saved_edit,data))
|
||||
try:
|
||||
data['output_dir']=str(access.output_path(request,data.get('output_dir','').strip(),folder_paths.get_output_directory()))
|
||||
return web.json_response(await asyncio.to_thread(export_saved_edit,data))
|
||||
except (ValueError,OSError,KeyError) as exc:raise web.HTTPBadRequest(text=str(exc))
|
||||
|
||||
@routes.post('/dogma/h3/v2/choose-folder')
|
||||
@@ -652,26 +650,39 @@ def register_routes():
|
||||
|
||||
@routes.get('/dogma/h3/v2/folders')
|
||||
async def folders(request):
|
||||
local_request(request)
|
||||
access.browser(request)
|
||||
import asyncio
|
||||
def browse():
|
||||
directory=Path(request.query.get('path') or folder_paths.get_output_directory()).expanduser().resolve()
|
||||
output=Path(folder_paths.get_output_directory()).resolve()
|
||||
local=access.is_local(request)
|
||||
directory=access.output_path(request,request.query.get('path'),output)
|
||||
if not directory.is_dir(): raise ValueError('Cartella inesistente. Inserisci un percorso valido.')
|
||||
directories=[]
|
||||
directories=[];files=[]
|
||||
media={'.mp4','.mov','.webm','.mkv','.png','.jpg','.jpeg','.webp','.gif','.wav','.mp3','.flac'}
|
||||
with os.scandir(directory) as entries:
|
||||
for entry in entries:
|
||||
try:
|
||||
if entry.is_dir(): directories.append({'name':entry.name,'path':entry.path})
|
||||
if entry.is_dir() and (local or Path(entry.path).resolve().is_relative_to(output)):
|
||||
directories.append({'name':entry.name,'path':entry.path})
|
||||
elif request.query.get('files')=='1' and entry.is_file() and Path(entry.name).suffix.lower() in media:
|
||||
resolved=Path(entry.path).resolve()
|
||||
if resolved.is_relative_to(output):
|
||||
rel=resolved.relative_to(output)
|
||||
files.append({'name':entry.name,'size':entry.stat().st_size,
|
||||
'video':{'filename':rel.name,'subfolder':rel.parent.as_posix(),'type':'output'}})
|
||||
except OSError: continue
|
||||
if len(directories)>500: break
|
||||
if len(directories)+len(files)>500: break
|
||||
directories.sort(key=lambda x:x['name'].casefold())
|
||||
files.sort(key=lambda x:x['name'].casefold())
|
||||
drives=[]
|
||||
if os.name=='nt':
|
||||
if local and os.name=='nt':
|
||||
import ctypes
|
||||
bits=ctypes.windll.kernel32.GetLogicalDrives()
|
||||
drives=[{'name':chr(65+i)+':','path':chr(65+i)+':/'} for i in range(26) if bits & (1<<i)]
|
||||
return {'path':str(directory),'parent':str(directory.parent) if directory.parent!=directory else None,
|
||||
'drives':drives,'directories':directories[:500],'truncated':len(directories)>500}
|
||||
parent=str(directory.parent) if directory.parent!=directory and (local or directory.parent.is_relative_to(output)) else None
|
||||
return {'path':str(directory),'parent':parent,'restricted':not local,
|
||||
'drives':drives,'directories':directories[:500],'files':files[:500],
|
||||
'truncated':len(directories)+len(files)>500}
|
||||
try: return web.json_response(await asyncio.to_thread(browse))
|
||||
except (ValueError,OSError) as exc: raise web.HTTPBadRequest(text=str(exc))
|
||||
|
||||
@@ -689,10 +700,11 @@ def register_routes():
|
||||
async def catalog(request):
|
||||
import asyncio,time
|
||||
nonlocal catalog_task,catalog_time
|
||||
capabilities=access.catalog(request,folder_paths.get_output_directory())
|
||||
if catalog_task is None or (catalog_task.done() and (catalog_task.exception() is not None or time.monotonic()-catalog_time>30)):
|
||||
catalog_time=time.monotonic()
|
||||
catalog_task=asyncio.create_task(asyncio.to_thread(build_catalog))
|
||||
return web.json_response(await asyncio.shield(catalog_task))
|
||||
return web.json_response({**await asyncio.shield(catalog_task),'access':capabilities},headers={'Cache-Control':'no-store'})
|
||||
|
||||
@routes.get('/dogma/h3/v2/session/{session}')
|
||||
async def session(request):
|
||||
|
||||
@@ -115,7 +115,7 @@ def apply_restitch(core,data):
|
||||
return current
|
||||
finally:temp.unlink(missing_ok=True)
|
||||
|
||||
def register_routes(routes,core,local_request):
|
||||
def register_routes(routes,core,authorize_request):
|
||||
from aiohttp import web
|
||||
jobs={};slot=asyncio.Semaphore(1)
|
||||
async def launch(kind,data):
|
||||
@@ -132,12 +132,12 @@ def register_routes(routes,core,local_request):
|
||||
except Exception as exc:job.update(status='error',message=str(exc))
|
||||
job['task']=asyncio.create_task(run());return web.json_response({'id':job['id']})
|
||||
@routes.post('/dogma/h3/v2/edit/restitch/preview')
|
||||
async def preview(request):local_request(request);return await launch('preview',await request.json())
|
||||
async def preview(request):authorize_request(request);return await launch('preview',await request.json())
|
||||
@routes.post('/dogma/h3/v2/edit/restitch/apply')
|
||||
async def apply(request):local_request(request);return await launch('apply',await request.json())
|
||||
async def apply(request):authorize_request(request);return await launch('apply',await request.json())
|
||||
@routes.get('/dogma/h3/v2/edit/restitch/job/{id}')
|
||||
async def status(request):
|
||||
local_request(request);job=jobs.get(request.match_info['id'])
|
||||
authorize_request(request);job=jobs.get(request.match_info['id'])
|
||||
if not job:raise web.HTTPNotFound(text='Lavorazione non disponibile.')
|
||||
return web.json_response({k:v for k,v in job.items() if k!='task'})
|
||||
async def cleanup(app):await asyncio.gather(*(j['task'] for j in jobs.values()),return_exceptions=True)
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
"""DOGMA Nodes: bounded-working-memory HDR finishing for ComfyUI >= 0.45.0.
|
||||
|
||||
No models, dependencies, monkey patches, or changes to ComfyUI are installed.
|
||||
The native VAE still returns a complete batch; finishing consumes one frame
|
||||
at a time and returns file paths, never another complete IMAGE batch.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
|
||||
import av
|
||||
import numpy as np
|
||||
import torch
|
||||
import safetensors.torch
|
||||
import folder_paths
|
||||
import nodes
|
||||
import comfy.utils
|
||||
from comfy.cli_args import args
|
||||
from comfy_api.latest import io, ui
|
||||
from comfy_api.latest._input_impl.video_types import (
|
||||
BT2020_NCL, set_video_color_properties,
|
||||
)
|
||||
from comfy_extras import nodes_images as ni
|
||||
|
||||
VERSION = "1.0.51"
|
||||
SPACES = ["sRGB", "HDR", "HDR ACEScct", "HDR LogC3", "HDR PQ", "linear", "linear ACEScg"]
|
||||
log = logging.getLogger("DOGMA.HDR")
|
||||
|
||||
|
||||
def require_fp32():
|
||||
if args.fp16_intermediates:
|
||||
raise ValueError("HDR 16-bit float: rimuovi --fp16-intermediates dall'avvio di Comfy. La conversione colore richiede intermedi fp32.")
|
||||
|
||||
|
||||
def convert_frame(frame, source, destination):
|
||||
require_fp32()
|
||||
return ni.ImageColorSpace.execute(
|
||||
frame.to(device="cpu", dtype=torch.float32), source, destination
|
||||
).result[0].to(device="cpu", dtype=torch.float32)
|
||||
|
||||
|
||||
class DogmaHDRColorChunked(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="DOGMAHDRColorChunkedV1", display_name="DOGMA HDR | Colore per fotogramma",
|
||||
category="DOGMA/HDR",
|
||||
description="Stessa trasformazione colore Comfy, con temporanei limitati a un fotogramma. Non riduce risoluzione o precisione.",
|
||||
inputs=[io.Image.Input("image"),
|
||||
io.Combo.Input("source", options=SPACES, default="sRGB"),
|
||||
io.Combo.Input("destination", options=SPACES, default="HDR ACEScct")],
|
||||
outputs=[io.Image.Output()],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, image, source, destination):
|
||||
require_fp32()
|
||||
# Per-frame tone mapping would differ from the native batch transform.
|
||||
# This node is intentionally limited to the non-tone-mapping HDR path.
|
||||
if destination == "sRGB" or (source == "HDR PQ" and destination == "HDR"):
|
||||
raise ValueError("Questa conversione usa statistiche dell'intero batch: usa ImageColorSpace nativo per il tonemapping SDR/PQ-HLG.")
|
||||
if source == destination:
|
||||
return io.NodeOutput(image.to(device="cpu", dtype=torch.float32))
|
||||
output = torch.empty(image.shape, device="cpu", dtype=torch.float32)
|
||||
progress = comfy.utils.ProgressBar(len(image))
|
||||
with torch.inference_mode():
|
||||
for index in range(len(image)):
|
||||
nodes.before_node_execution()
|
||||
output[index:index + 1].copy_(convert_frame(image[index:index + 1], source, destination))
|
||||
progress.update(1)
|
||||
return io.NodeOutput(output)
|
||||
|
||||
|
||||
def fit_frame(frame, source_width, source_height, target_width, target_height):
|
||||
width = 2 * math.floor(min(target_width, target_height * source_width / source_height) / 2)
|
||||
height = 2 * math.floor(min(target_height, target_width * source_height / source_width) / 2)
|
||||
if min(width, height) < 2:
|
||||
raise ValueError("Proporzioni del sorgente non valide.")
|
||||
frame = nodes.ImageScale().upscale(frame, "bicubic", width, height, "disabled")[0]
|
||||
return ni.ResizeAndPadImage.execute(frame, target_width, target_height, "black", "area").result[0]
|
||||
|
||||
|
||||
def create_job(prefix, output_directory):
|
||||
# Comfy validates that the requested prefix remains inside its output root.
|
||||
directory, name, counter, _, _ = folder_paths.get_save_image_path(prefix, output_directory)
|
||||
while True:
|
||||
job = Path(directory) / f"{name}_{counter:05d}"
|
||||
try:
|
||||
job.mkdir()
|
||||
return job
|
||||
except FileExistsError:
|
||||
counter += 1
|
||||
|
||||
|
||||
def write_json(path, data):
|
||||
temporary = path.with_suffix(path.suffix + ".partial")
|
||||
temporary.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def encode_audio(container, audio, count, rate):
|
||||
if audio is None:
|
||||
return
|
||||
sample_rate = int(audio["sample_rate"])
|
||||
wave = audio["waveform"][0]
|
||||
layouts = {1: "mono", 2: "stereo", 3: "2.1", 4: "quad", 6: "5.1", 8: "7.1"}
|
||||
if wave.shape[0] not in layouts:
|
||||
raise ValueError(f"Audio: {wave.shape[0]} canali non supportati; nessun downmix automatico.")
|
||||
layout = layouts[wave.shape[0]]
|
||||
stream = container.add_stream("pcm_s24le", rate=sample_rate, layout=layout)
|
||||
n = min(wave.shape[-1], math.ceil(Fraction(sample_rate * count, 1) / rate))
|
||||
# Audio is also chunked, avoiding an extra full-length contiguous copy.
|
||||
for offset in range(0, n, 8192):
|
||||
part = wave[:, offset:min(n, offset + 8192)].float().cpu().contiguous().numpy()
|
||||
frame = av.AudioFrame.from_ndarray(part, format="fltp", layout=layout)
|
||||
frame.sample_rate = sample_rate
|
||||
frame.time_base = Fraction(1, sample_rate)
|
||||
frame.pts = offset
|
||||
container.mux(stream.encode(frame))
|
||||
container.mux(stream.encode(None))
|
||||
|
||||
|
||||
def export_frames(images, source_width, source_height, source_frames, fps,
|
||||
filename_prefix="DOGMA_HDR_4K/master", save_mov=True, audio=None,
|
||||
target_width=3840, target_height=2160, output_directory=None,
|
||||
progress=None, metadata=None):
|
||||
"""Sequential finishing; target size override is for isolated CPU QA only."""
|
||||
require_fp32()
|
||||
if images.ndim != 4 or images.shape[-1] != 3:
|
||||
raise ValueError("Atteso IMAGE RGB [fotogrammi, altezza, larghezza, 3] in ACEScct.")
|
||||
if source_width < 1 or source_height < 1 or source_frames < 1 or source_frames > len(images):
|
||||
raise ValueError(f"Conteggio/proporzioni non validi: richiesti {source_frames} frame, decode disponibile {len(images)}.")
|
||||
if not math.isfinite(float(fps)) or fps <= 0:
|
||||
raise ValueError("FPS sorgente non valido.")
|
||||
# Recover standard rational rates without rounding 24000/1001 to 23.976.
|
||||
rate = Fraction(float(fps)).limit_denominator(1000000)
|
||||
output_directory = output_directory or folder_paths.get_output_directory()
|
||||
job = create_job(filename_prefix, output_directory)
|
||||
exr_dir = job / "EXR"
|
||||
exr_dir.mkdir()
|
||||
mov_path = job / "DOGMA_4K_HDR_HLG.mov"
|
||||
mov_temporary = job / "DOGMA_4K_HDR_HLG.partial.mov"
|
||||
manifest = dict(version=VERSION, status="incomplete", completed_frames=0,
|
||||
frames=source_frames, fps=str(rate), width=target_width, height=target_height,
|
||||
master="ACEScg AP1/D60 RGB HALF EXR ZIP16", source_width=source_width,
|
||||
source_height=source_height, decoded_frames=len(images),
|
||||
mov="ProRes 4444 BT.2020 HLG; 10-bit encoding input; PCM24 original audio" if save_mov else None,
|
||||
metadata=metadata or {})
|
||||
write_json(job / "manifest.json", manifest)
|
||||
container = None
|
||||
try:
|
||||
if save_mov:
|
||||
container = av.open(str(mov_temporary), mode="w", format="mov", options={"movflags": "use_metadata_tags+faststart"})
|
||||
stream = container.add_stream("prores_ks", rate=rate)
|
||||
stream.width, stream.height = target_width, target_height
|
||||
stream.pix_fmt = "yuv444p10le"
|
||||
stream.options = {"profile": "4444", "alpha_bits": "16"}
|
||||
stream.codec_context.thread_count = 2
|
||||
stream.time_base = 1 / rate
|
||||
set_video_color_properties(stream.codec_context, "HDR")
|
||||
# Register audio before the muxer writes the MOV header.
|
||||
# The PCM stream is created below by encode_audio, before video encode.
|
||||
if audio is not None:
|
||||
encode_audio(container, audio, source_frames, rate)
|
||||
with torch.inference_mode():
|
||||
for index in range(source_frames):
|
||||
nodes.before_node_execution()
|
||||
linear = convert_frame(images[index:index + 1], "HDR ACEScct", "linear ACEScg")
|
||||
linear = fit_frame(linear, source_width, source_height, target_width, target_height)
|
||||
if not torch.isfinite(linear).all():
|
||||
raise ValueError(f"Frame {index}: valori HDR NaN/Inf. Il file non viene dichiarato completo.")
|
||||
# HALF cannot represent values outside +-65504. Do not silently clip.
|
||||
if linear.abs().max() > 65504:
|
||||
raise ValueError(f"Frame {index}: range oltre HALF (+-65504). Serve EXR float32 per questi valori.")
|
||||
encoded = ni._encode_image(linear[0], "exr", "16-bit float", "linear ACEScg")
|
||||
encoded = ni.inject_exr_metadata(encoded, None, None, "linear ACEScg")
|
||||
exr = exr_dir / f"frame_{index:06d}.exr"
|
||||
temporary = exr.with_suffix(".exr.partial")
|
||||
temporary.write_bytes(encoded)
|
||||
os.replace(temporary, exr)
|
||||
del encoded
|
||||
if container is not None:
|
||||
hlg = convert_frame(linear, "linear ACEScg", "HDR")[0]
|
||||
pixels = (hlg * 65535).clamp(0, 65535).numpy().astype(np.uint16)
|
||||
frame = av.VideoFrame.from_ndarray(pixels, format="rgb48le")
|
||||
frame = frame.reformat(format="yuv444p10le", dst_colorspace=BT2020_NCL)
|
||||
set_video_color_properties(frame, "HDR")
|
||||
frame.time_base = 1 / rate
|
||||
frame.pts = index
|
||||
container.mux(stream.encode(frame))
|
||||
del hlg, pixels, frame
|
||||
del linear
|
||||
manifest["completed_frames"] = index + 1
|
||||
if (index + 1) % 12 == 0 or index + 1 == source_frames:
|
||||
write_json(job / "manifest.json", manifest)
|
||||
log.info("HDR export: %d/%d frame; %s", index + 1, source_frames, job)
|
||||
if progress is not None:
|
||||
progress.update(1)
|
||||
if container is not None:
|
||||
container.mux(stream.encode(None))
|
||||
container.close()
|
||||
container = None
|
||||
os.replace(mov_temporary, mov_path)
|
||||
manifest["status"] = "complete"
|
||||
write_json(job / "manifest.json", manifest)
|
||||
except BaseException as error:
|
||||
manifest["error"] = str(error)
|
||||
write_json(job / "manifest.json", manifest)
|
||||
raise
|
||||
finally:
|
||||
if container is not None:
|
||||
container.close()
|
||||
return job, mov_path if save_mov else None
|
||||
|
||||
|
||||
class DogmaHDRSaveStreaming(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="DOGMAHDRSaveStreamingV1", display_name="DOGMA HDR | Salva 4K EXR16 + MOV progressivo",
|
||||
category="DOGMA/HDR", is_output_node=True,
|
||||
description="Converte ACEScct, ripristina proporzioni, elimina la sola coda temporale e salva un frame alla volta. Nessun secondo batch IMAGE completo.",
|
||||
inputs=[io.Image.Input("images"), io.Int.Input("source_width", force_input=True),
|
||||
io.Int.Input("source_height", force_input=True), io.Int.Input("source_frames", force_input=True),
|
||||
io.Float.Input("fps", force_input=True),
|
||||
io.String.Input("filename_prefix", default="DOGMA_HDR_4K/master"),
|
||||
io.Boolean.Input("save_mov", default=True), io.Audio.Input("audio", optional=True)],
|
||||
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
|
||||
outputs=[io.String.Output("exr_directory"), io.String.Output("mov_path")],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, images, source_width, source_height, source_frames, fps,
|
||||
filename_prefix="DOGMA_HDR_4K/master", save_mov=True, audio=None):
|
||||
metadata = {}
|
||||
if not args.disable_metadata and cls.hidden is not None:
|
||||
if cls.hidden.extra_pnginfo:
|
||||
metadata.update(cls.hidden.extra_pnginfo)
|
||||
if cls.hidden.prompt:
|
||||
metadata["prompt"] = cls.hidden.prompt
|
||||
job, mov = export_frames(images, source_width, source_height, source_frames, fps,
|
||||
filename_prefix, save_mov, audio,
|
||||
progress=comfy.utils.ProgressBar(source_frames), metadata=metadata)
|
||||
preview = None
|
||||
if mov:
|
||||
subfolder = str(job.relative_to(Path(folder_paths.get_output_directory()))).replace("\\", "/")
|
||||
preview = ui.PreviewVideo([ui.SavedResult(mov.name, subfolder, io.FolderType.output)])
|
||||
return io.NodeOutput(str(job / "EXR"), str(mov) if mov else "", ui=preview)
|
||||
|
||||
|
||||
class DogmaHDRCheckpoint(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="DOGMAHDRCheckpointV1", display_name="DOGMA HDR | Checkpoint prima del decode",
|
||||
category="DOGMA/HDR",
|
||||
description="Salva il latente prima del decode. Per riprendere lo stesso video inserisci il percorso del checkpoint: i sampler vengono saltati.",
|
||||
inputs=[io.Latent.Input("samples", lazy=True),
|
||||
io.String.Input("filename_prefix", default="DOGMA_HDR_4K/checkpoints/HDR"),
|
||||
io.String.Input("resume_from", default="", tooltip="Lascia vuoto per una nuova generazione. Percorso .latent relativo a output, oppure assoluto dentro input/output Comfy.")],
|
||||
outputs=[io.Latent.Output(), io.String.Output("checkpoint_path")],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def check_lazy_status(cls, filename_prefix, resume_from, samples=None):
|
||||
return [] if resume_from.strip() or samples is not None else ["samples"]
|
||||
|
||||
@classmethod
|
||||
def execute(cls, filename_prefix, resume_from="", samples=None):
|
||||
if resume_from.strip():
|
||||
path = Path(resume_from.strip()).expanduser()
|
||||
if not path.is_absolute():
|
||||
path = Path(folder_paths.get_output_directory()) / path
|
||||
path = path.resolve()
|
||||
roots = [Path(folder_paths.get_output_directory()).resolve(), Path(folder_paths.get_input_directory()).resolve()]
|
||||
if not any(path.is_relative_to(root) for root in roots) or path.suffix != ".latent":
|
||||
raise ValueError("Il checkpoint deve essere un file .latent nella cartella input/output di Comfy.")
|
||||
saved = safetensors.torch.load_file(str(path), device="cpu")
|
||||
if "latent_format_version_0" not in saved or "latent_tensor" not in saved:
|
||||
raise ValueError("Checkpoint non compatibile: atteso formato latente Comfy attuale.")
|
||||
return io.NodeOutput({"samples": saved["latent_tensor"]}, str(path))
|
||||
if samples is None or "samples" not in samples:
|
||||
raise ValueError("Nessun latente ricevuto dal sampler.")
|
||||
directory, name, counter, _, _ = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory())
|
||||
path = Path(directory) / f"{name}_{counter:05d}_.latent"
|
||||
temporary = path.with_suffix(".latent.partial")
|
||||
saved = {"latent_tensor": samples["samples"].detach().contiguous(),
|
||||
"latent_format_version_0": torch.tensor([])}
|
||||
comfy.utils.save_torch_file(saved, str(temporary), metadata={"dogma_hdr_streaming": VERSION})
|
||||
os.replace(temporary, path)
|
||||
log.info("Checkpoint HDR salvato prima del decode: %s", path)
|
||||
return io.NodeOutput(samples, str(path))
|
||||
|
||||
|
||||
# Registered through the existing DOGMA package, not a separate extension.
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DOGMAHDRColorChunkedV1": DogmaHDRColorChunked,
|
||||
"DOGMAHDRSaveStreamingV1": DogmaHDRSaveStreaming,
|
||||
"DOGMAHDRCheckpointV1": DogmaHDRCheckpoint,
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DOGMAHDRColorChunkedV1": "DOGMA HDR | Colore per fotogramma",
|
||||
"DOGMAHDRSaveStreamingV1": "DOGMA HDR | Salva 4K EXR16 + MOV progressivo",
|
||||
"DOGMAHDRCheckpointV1": "DOGMA HDR | Checkpoint prima del decode",
|
||||
}
|
||||
# Initialize V3 schema fields while retaining explicit, Registry-readable IDs.
|
||||
for _node in NODE_CLASS_MAPPINGS.values():
|
||||
_node.GET_SCHEMA()
|
||||
del _node
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-dogma-nodes"
|
||||
description = "Custom ComfyUI nodes for DOGMA AI video workflows."
|
||||
version = "1.0.49"
|
||||
version = "1.0.51"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["scipy>=1.10"]
|
||||
|
||||
|
||||
@@ -67,4 +67,7 @@
|
||||
.dg-studio .dg-upload-status{font-size:12px;margin:6px 0;overflow-wrap:anywhere;min-height:0}
|
||||
.dg-studio .dg-file-drop[data-upload-state=error]>.dg-upload-status{color:#ffbb98}
|
||||
.dg-studio .dg-file-drop[aria-busy=true]>.dg-drop-hint{color:#ffd2b8;border-style:solid}
|
||||
.dg-studio .dg-folders a{display:flex;align-items:center;gap:8px;padding:10px 14px;margin:4px 6px 4px 0;border:1px solid #505256;border-radius:7px;background:#26282b;text-decoration:none;overflow-wrap:anywhere}
|
||||
.dg-studio .dg-folders a:hover{border-color:var(--orange)}
|
||||
.dg-studio [hidden]{display:none!important}
|
||||
.dg-studio .dg-upload-toast{position:fixed;bottom:45px;left:50%;transform:translateX(-50%);z-index:20;max-width:80vw;padding:10px 18px;border:1px solid var(--orange);border-radius:7px;background:#30231d;box-shadow:0 4px 20px #0008;color:#fff}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import {actionButton,decorateActions,setAction} from './dogma_h3_actions.js?v=studio-1-0-49';
|
||||
import {assetField,referenceDrop,guardFileNavigation} from './dogma_h3_uploads.js?v=studio-1-0-49';
|
||||
import { renderEditor, renderEditResults, renderEditTrials } from './dogma_h3_video_edit.js?v=studio-1-0-49';
|
||||
import {actionButton,decorateActions,setAction} from './dogma_h3_actions.js?v=studio-1-0-50';
|
||||
import {assetField,referenceDrop,guardFileNavigation} from './dogma_h3_uploads.js?v=studio-1-0-50';
|
||||
import { renderEditor, renderEditResults, renderEditTrials } from './dogma_h3_video_edit.js?v=studio-1-0-50';
|
||||
|
||||
import { inputFile, viewURL, mediaPreview, releaseMedia } from './dogma_h3_media.js?v=studio-1-0-49';
|
||||
import { inputFile, viewURL, mediaPreview, releaseMedia } from './dogma_h3_media.js?v=studio-1-0-50';
|
||||
|
||||
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=studio-1-0-49',ROOT);document.head.append(css);
|
||||
const css=document.createElement('link');css.rel='stylesheet';css.href=new URL('dogma_h3_director_v134.css?v=studio-1-0-50',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=actionButton;
|
||||
@@ -40,6 +40,12 @@ export class DirectorStudio {
|
||||
let stored;try{stored=JSON.parse(this.widget.value);}catch{stored={};}
|
||||
this.config=merge(clone(this.catalog.defaults),stored);
|
||||
this.config.session ||= crypto.randomUUID();
|
||||
const access=this.catalog.access;
|
||||
if(access?.local_files===false&&access.platform!=='nt'&&access.output_directory){
|
||||
for(const [object,key]of [[this.config,'output_dir'],[this.config.edit,'final_output_dir']]){
|
||||
if(/^(?:[a-z]:[\\/]|\\\\)/i.test(object[key]||''))object[key]=key==='final_output_dir'?'':access.output_directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
family(task){return ['ref2v','edit'].includes(task)?'ref2v':'fl2v';}
|
||||
turbo(task,steps){
|
||||
@@ -97,10 +103,12 @@ export class DirectorStudio {
|
||||
clearTimeout(this.saveTimer);this.saveTimer=setTimeout(()=>{try{app.graph?.change?.();}catch(e){console.warn('DOGMA: aggiornamento workflow',e);}},250);
|
||||
}
|
||||
async request(path,opts={}){
|
||||
const headers=Object.fromEntries(new Headers(opts.headers||{}).entries());
|
||||
if(path.startsWith('/dogma/h3/v2/')&&this.catalog?.access?.token)headers['X-Dogma-Access']=this.catalog.access.token;
|
||||
const controller=new AbortController(),epoch=this.epoch;this.requests.add(controller);let expired=false;
|
||||
const timeout=opts.body instanceof FormData?null:setTimeout(()=>{expired=true;controller.abort();},20000);
|
||||
try{
|
||||
const r=await api.fetchApi(path,{...opts,signal:opts.signal?AbortSignal.any([controller.signal,opts.signal]):controller.signal});
|
||||
const r=await api.fetchApi(path,{...opts,headers,signal:opts.signal?AbortSignal.any([controller.signal,opts.signal]):controller.signal});
|
||||
if(!r.ok)throw new Error(`Comfy: ${r.status} ${await r.text()}`);
|
||||
const result=await r.json();
|
||||
if(epoch!==this.epoch){const e=new Error('Pannello chiuso');e.name='AbortError';throw e;}
|
||||
@@ -146,27 +154,32 @@ export class DirectorStudio {
|
||||
this.save();
|
||||
}
|
||||
destroy(){this.close();clearTimeout(this.saveTimer);this.abort.abort();}
|
||||
async browseFolders(scope='general'){
|
||||
async browseFolders(scope='general',options={}){
|
||||
if(this.folderPanel||!this.dialog)return;
|
||||
const epoch=this.epoch,panel=el('section','','dg-folder-browser');this.folderPanel=panel;
|
||||
panel.setAttribute('role','region');panel.setAttribute('aria-label','Scegli cartella output');
|
||||
panel.setAttribute('role','region');panel.setAttribute('aria-label',options.viewOnly?'Cartella del video salvato':'Scegli cartella output');
|
||||
const main=this.dialog.querySelector('main');main.prepend(panel);
|
||||
const target=scope==='edit'?this.config.edit:this.config,key=scope==='edit'?'final_output_dir':'output_dir';
|
||||
const input=el('input');input.type='text';input.value=target[key]||this.config.output_dir||'';input.setAttribute('aria-label','Percorso cartella');
|
||||
const remote=this.catalog.access?.local_files===false;
|
||||
const serverRoot=(this.catalog.access?.output_directory||'').replaceAll('\\','/').replace(/\/$/,'');
|
||||
const preferred=(target[key]||this.config.output_dir||'').replaceAll('\\','/');
|
||||
const initial=remote&&preferred!==serverRoot&&!preferred.startsWith(serverRoot+'/')?serverRoot:preferred;
|
||||
const input=el('input');input.type='text';input.value=options.initialPath||initial;input.setAttribute('aria-label','Percorso cartella');
|
||||
const info=el('p'),list=el('div','','dg-folders');let busy=false,current='';
|
||||
const done=()=>{panel.remove();if(this.folderPanel===panel)this.folderPanel=null;};
|
||||
const use=button('Usa questa cartella',async()=>{if(input.value!==current&&!await load(input.value))return;target[key]=current;this.save();done();this.render();},'dg-primary');use.disabled=true;
|
||||
const use=button('Usa questa cartella',async()=>{if(input.value!==current&&!await load(input.value))return;target[key]=current;this.save();done();this.render();},'dg-primary');use.disabled=true;use.hidden=!!options.viewOnly;
|
||||
const go=button('Vai',()=>load(input.value));
|
||||
panel.append(el('h2','Cartella di salvataggio'),el('p','Naviga nelle cartelle del computer che esegue Comfy.'),input,go,button('Annulla',done),use,info,list);
|
||||
panel.append(el('h2',options.viewOnly?'Cartella del video salvato':'Cartella di salvataggio'),el('p',remote?'Cartelle del server che esegue Comfy. La navigazione parte dalla sua cartella output.':'Naviga nelle cartelle del computer che esegue Comfy.'),input,go,button(options.viewOnly?'Chiudi':'Annulla',done),use,info,list);
|
||||
const load=async path=>{
|
||||
if(busy)return;busy=true;go.disabled=true;use.disabled=true;info.textContent='Caricamento cartelle…';list.replaceChildren();
|
||||
try{
|
||||
const r=await this.request('/dogma/h3/v2/folders?'+new URLSearchParams({path:path||''}));
|
||||
const r=await this.request('/dogma/h3/v2/folders?'+new URLSearchParams({path:path||'',files:options.viewOnly?'1':'0'}));
|
||||
if(epoch!==this.epoch||this.folderPanel!==panel)return;
|
||||
current=r.path;input.value=current;info.textContent=r.truncated?'Mostrate le prime 500 cartelle. Puoi inserire il percorso completo.':'Seleziona una sottocartella oppure conferma questa cartella.';
|
||||
current=r.path;input.value=current;info.textContent=r.truncated?'Mostrati i primi 500 elementi.':options.viewOnly?'File della cartella: clicca un nome per scaricarlo.':'Seleziona una sottocartella oppure conferma questa cartella.';
|
||||
if(r.parent)list.append(button('↑ Cartella superiore',()=>load(r.parent)));
|
||||
for(const d of r.drives||[])list.append(button(d.name,()=>load(d.path)));
|
||||
for(const d of r.directories||[])list.append(button('📁 '+d.name,()=>load(d.path)));
|
||||
for(const f of r.files||[]){const link=el('a');setAction(link,f.name+' · '+(f.size/1024/1024).toFixed(1)+' MB','download');link.href=viewURL(f.video);link.download=f.name;list.append(link);}
|
||||
use.disabled=false;return true;
|
||||
}catch(e){if(epoch===this.epoch&&this.folderPanel===panel&&e.name!=='AbortError')info.textContent=e.message;return false;}
|
||||
finally{busy=false;go.disabled=false;}
|
||||
@@ -254,7 +267,7 @@ export class DirectorStudio {
|
||||
if(previewEnabled)required.push('ModelPreviewOverrideKJ');
|
||||
if(action==='finish'&&this.config.task!=='edit'&&this.config.finish.stages!=='ultimate')required.push('MinimaxH3LatentUpscaler3D');
|
||||
if(action==='finish'&&this.config.task!=='edit'&&this.config.finish.stages!=='latent')required.push('UltimateSDUpscaleNoUpscaleGuider');
|
||||
const missing=required.filter(n=>!this.catalog.nodes.includes(n));if(missing.length)throw new Error('Installa dal Manager i nodi richiesti: '+missing.join(', '));
|
||||
const missing=required.filter(n=>!this.catalog.nodes.includes(n));if(missing.length)throw new Error('Installa dal Manager: '+missing.map(n=>n==='MVEx_MaskToLatentSpace'?'MaskVidExperiments (nodo '+n+')':n).join(', '));
|
||||
const q=await this.request('/queue');if(q.queue_running?.length||q.queue_pending?.length)throw new Error('Comfy sta già lavorando. Attendi la fine della coda.');
|
||||
this.config.action=action;this.config.nonce++;this.save();
|
||||
if(this.config.task==='edit'&&action==='draft'){delete this.data.edit_batch;this.promotedTrial=null;this.editFinishWaiting=null;}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {mediaPreview,inputFile} from './dogma_h3_media.js?v=studio-1-0-49';
|
||||
import {frameIndex,frameTime,frameCount,stepTime} from './dogma_h3_timing.js?v=studio-1-0-49';
|
||||
import {actionButton,setAction,loraLabel} from './dogma_h3_actions.js?v=studio-1-0-49';
|
||||
import {mediaPreview,inputFile} from './dogma_h3_media.js?v=studio-1-0-50';
|
||||
import {frameIndex,frameTime,frameCount,stepTime} from './dogma_h3_timing.js?v=studio-1-0-50';
|
||||
import {actionButton,setAction,loraLabel} from './dogma_h3_actions.js?v=studio-1-0-50';
|
||||
const el=(tag,text='')=>{const e=document.createElement(tag);e.textContent=text;return e;};
|
||||
const button=actionButton;
|
||||
const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mediaPreview } from './dogma_h3_media.js?v=studio-1-0-49';
|
||||
import { mediaPreview } from './dogma_h3_media.js?v=studio-1-0-50';
|
||||
|
||||
const el=(tag,text='')=>{const e=document.createElement(tag);e.textContent=text;return e;};
|
||||
export function maskKey(c){
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {viewURL} from './dogma_h3_media.js?v=studio-1-0-49';
|
||||
import {actionButton,setAction} from './dogma_h3_actions.js?v=studio-1-0-49';
|
||||
import {RestitchHover} from './dogma_h3_restitch_hover.js?v=studio-1-0-49';
|
||||
import {viewURL} from './dogma_h3_media.js?v=studio-1-0-50';
|
||||
import {actionButton,setAction} from './dogma_h3_actions.js?v=studio-1-0-50';
|
||||
import {RestitchHover} from './dogma_h3_restitch_hover.js?v=studio-1-0-50';
|
||||
const el=(tag,text='',cls='')=>{const e=document.createElement(tag);e.textContent=text;e.className=cls;return e;};
|
||||
const clamp=(x,a,b)=>Math.min(b,Math.max(a,x));
|
||||
export class RestitchPanel{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {viewURL} from './dogma_h3_media.js?v=studio-1-0-49';
|
||||
import {viewURL} from './dogma_h3_media.js?v=studio-1-0-50';
|
||||
const clamp=(x,a,b)=>Math.min(b,Math.max(a,x));
|
||||
const el=(tag,cls='')=>{const e=document.createElement(tag);e.className=cls;return e;};
|
||||
export class RestitchHover{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {mediaPreview,inputFile} from './dogma_h3_media.js?v=studio-1-0-49';
|
||||
import {actionButton,setAction} from './dogma_h3_actions.js?v=studio-1-0-49';
|
||||
import {mediaPreview,inputFile} from './dogma_h3_media.js?v=studio-1-0-50';
|
||||
import {actionButton,setAction} from './dogma_h3_actions.js?v=studio-1-0-50';
|
||||
const el=(tag,text='',cls='')=>{const e=document.createElement(tag);e.textContent=text;e.className=cls;return e;};
|
||||
const extensions={image:new Set(['png','jpg','jpeg','webp','gif','bmp','tif','tiff','avif','heic','heif']),video:new Set(['mp4','mov','m4v','mkv','webm','avi','mpeg','mpg','wmv','flv','ts','mts','m2ts','mxf','ogv','3gp'])};
|
||||
export function mediaKind(file){const mime=String(file.type||'').toLowerCase();if(mime.startsWith('image/'))return 'image';if(mime.startsWith('video/'))return 'video';const ext=String(file.name||'').split('.').pop().toLowerCase();return Object.keys(extensions).find(kind=>extensions[kind].has(ext));}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { MaskEditor, compositeControls } from './dogma_h3_edit_mask.js?v=studio-1-0-49';
|
||||
import { EditComparison } from './dogma_h3_edit_compare.js?v=studio-1-0-49';
|
||||
import { MaskEditor, compositeControls } from './dogma_h3_edit_mask.js?v=studio-1-0-50';
|
||||
import { EditComparison } from './dogma_h3_edit_compare.js?v=studio-1-0-50';
|
||||
import { api } from '../../scripts/api.js';
|
||||
import {actionButton,setAction,loraLabel} from './dogma_h3_actions.js?v=studio-1-0-49';
|
||||
import {RestitchPanel} from './dogma_h3_restitch.js?v=studio-1-0-49';
|
||||
import {assetField,referenceDrop} from './dogma_h3_uploads.js?v=studio-1-0-49';
|
||||
import { inputFile, viewURL, mediaPreview } from './dogma_h3_media.js?v=studio-1-0-49';
|
||||
import { frameIndex, frameTime, frameCount, snapSelection, stepTime, parseSelection } from './dogma_h3_timing.js?v=studio-1-0-49';
|
||||
export { snapSelection } from './dogma_h3_timing.js?v=studio-1-0-49';
|
||||
import {actionButton,setAction,loraLabel} from './dogma_h3_actions.js?v=studio-1-0-50';
|
||||
import {RestitchPanel} from './dogma_h3_restitch.js?v=studio-1-0-50';
|
||||
import {assetField,referenceDrop} from './dogma_h3_uploads.js?v=studio-1-0-50';
|
||||
import { inputFile, viewURL, mediaPreview } from './dogma_h3_media.js?v=studio-1-0-50';
|
||||
import { frameIndex, frameTime, frameCount, snapSelection, stepTime, parseSelection } from './dogma_h3_timing.js?v=studio-1-0-50';
|
||||
export { snapSelection } from './dogma_h3_timing.js?v=studio-1-0-50';
|
||||
|
||||
const el=(tag,text='',cls='')=>{const e=document.createElement(tag);e.textContent=text;e.className=cls;return e;};
|
||||
const button=actionButton;
|
||||
@@ -131,7 +131,14 @@ export function renderEditResults(studio,body){
|
||||
studio.editCompare=new EditComparison(studio,card,selected);
|
||||
card.append(el('small',selected.saved_path));
|
||||
if(selected.export_error)card.append(el('p','Il video completo è conservato nella sessione. La copia nella cartella scelta non è riuscita: '+selected.export_error));
|
||||
card.append(button('Apri cartella del video salvato',async()=>{try{await studio.request('/dogma/h3/v2/open-folder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session:studio.config.session,id:selected.id})});}catch(e){studio.fail(e.message);}}));
|
||||
card.append(button('Apri cartella del video salvato',async()=>{try{
|
||||
if(studio.catalog.access?.open_folder===false){
|
||||
const base=studio.catalog.access.output_directory.replaceAll('\\','/');
|
||||
const saved=String(selected.saved_path||'').replaceAll('\\','/'),directory=saved.slice(0,saved.lastIndexOf('/'));
|
||||
const initialPath=directory===base||directory.startsWith(base+'/')?directory:base+'/'+(selected.video.subfolder||'');
|
||||
await studio.browseFolders('edit',{initialPath,viewOnly:true});
|
||||
}else await studio.request('/dogma/h3/v2/open-folder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session:studio.config.session,id:selected.id})});
|
||||
}catch(e){studio.fail(e.message);}}));
|
||||
const copy=button('Copia questo finale nella cartella scelta',async()=>{if(copy.disabled)return;copy.disabled=true;try{const result=await studio.request('/dogma/h3/v2/edit/export-copy',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session:studio.config.session,id:selected.id,output_dir:studio.config.edit.final_output_dir||studio.config.output_dir})});for(const item of studio.data.edits||[])if(item.id===result.id){item.saved_path=result.saved_path;delete item.export_error;}studio.data.status='done';studio.data.message='Video completo copiato nella cartella scelta.';studio.render();}catch(e){studio.fail(e.message);}finally{copy.disabled=false;}});copy.disabled=studio.pending;card.append(copy);
|
||||
card.append(el('small','La copia usa il video già completato, senza ricalcolarlo. Destinazione: '+(studio.config.edit.final_output_dir||studio.config.output_dir||'cartella output Comfy')));
|
||||
card.append(button('Cambia cartella di destinazione…',()=>studio.browseFolders('edit')));
|
||||
|
||||
Reference in New Issue
Block a user