Start loads and generates. Pause retains the model. Stop finishes active work and releases the model.
memory_budget
float
18.5
18.5 … 32
Estimated total worker commit budget in GiB on Windows. Startup subtracts memory already held by this worker, with at least 2 GiB free required. Wait and retry if headroom is insufficient. Increase for more margin; set before Start.
model
Name
Type
Default
Range
Doc
root
string
""
Absolute path to the prepared FluxRT checkout. Restart after changing it.
prompt
string
Turn this into watercolor art.
Editing instruction. Apply changes at an inference boundary.
prompt_b
string
""
Second editing instruction. Empty uses prompt A at both ends. Encoded only when its text changes.
prompt_mix
float
0
0 … 1
Blend prompt embeddings: 0 is prompt A, 1 is prompt B. Uses control smoothing; does not add a model pass.
seed
int
52
0 … 2147483647
Keep fixed for continuity. Changes blend noise over transition/duration, with RIFE between generated frames.
steps
int
2
1 … 8
Denoising steps. More steps cost time; above four, spatial caching is disabled to limit VRAM.
guidance
float
1
1 … 3
Prompt guidance (CFG). Above one adds a second model pass per step and can exaggerate colors and detail.
source
Name
Type
Default
Range
Doc
width
int
320
256 … 512
Output width. Set before generation; restart the node to change size.
height
int
320
256 … 512
Output height. Set before generation; restart the node to change size.
influence
float
1
0 … 2
Source-image attention weight. Zero blocks source tokens, one is normal, two increases their influence. Not a denoising-strength percentage.
freeze
bool
false
Hold the current guide frame while controls remain live. Unfreeze accepts new input. Start while frozen captures the first frame.
reference
Name
Type
Default
Range
Doc
enabled
bool
false
Enable the captured reference live. Disable fades its weight to zero, then removes its image tokens.
main_image
string
source
source | reference
Choose which image is the main one to transform. The other supplies additional conditioning. Update prompt instructions to match these roles. Changes apply between generations.
capture
pulse
null
Capture the current reference input and blend the reference over transition/duration. Cut mode or zero duration applies it immediately.
influence
float
1
0 … 8
Reference attention weight. One is normal. Values above two are experimental and can copy reference objects; this is not pure style transfer.
smoothing
Name
Type
Default
Range
Doc
time
float
0.25
0 … 5
Seconds to approach 95% of a new CFG, image weight, reference weight, prompt mix or feedback target. Zero is immediate. New targets do not queue.
output_fps
float
0
0 … 60
Target playback FPS. Zero delivers only the selected RIFE frames. Above zero blends between RIFE frames on a paced clock without extra model passes. Applies at the next batch; can soften detail or ghost motion. Does not increase generation FPS. Keep common/max_frequency above this target.
transition
Name
Type
Default
Range
Doc
mode
string
prompt blend
cut | prompt blend
Blend prompts, seed noise, and reference images with RIFE, or cut to the new settings.
duration
float
2.5
0 … 60
Seconds for prompt, seed, and reference blends. Encoding, inference and playback add delay. Zero cuts. Changes restart the remaining blend from its current state.
frames
int
4
1 … 8
1 disables RIFE; 4 or 8 adds interpolated output frames. More frames do not increase model generation speed.
cut
pulse
null
Immediately apply all current targets and clear feedback and RIFE history at the next generation. Skip the current transition.
consistency
Name
Type
Default
Range
Doc
strength
float
0
0 … 0.8
Previous-output contribution to the source. Zero disables feedback. Start near 0.2; high values can cause drift or trails and reduce generation speed.
align_motion
bool
true
Warp feedback with source motion. Reject uncertain motion, changed regions, and exposed borders.
reset
pulse
null
Clear feedback, model cache, and RIFE history at the next inference boundary.
common
Name
Type
Default
Range
Doc
autotrigger
bool
true
max_frequency
float
60
1 … 120
Source
The current source of this node, as it stands in the goofi repository at node-bundles/image-generation/flux_rt.py.
node-bundles/image-generation/flux_rt.py
"""Edit image streams with a persistent, optional FluxRT runtime."""importjsonimportmathimportosfromcollectionsimportdequefrompathlibimportPathimportthreadingimporttimefromtypingimportNamedTupleimportcv2importnumpyasnpfromPILimportImagefromfluxrt.stream_processor.model_inference_subprocessimportModelInferenceSubprocessimportgoofidefavailable_commit():"""Read Windows commit headroom, including the paging file."""ifos.name!='nt':returnNoneimportctypesclassMemoryStatus(ctypes.Structure):_fields_=[('length',ctypes.c_uint32),('load',ctypes.c_uint32)]+[(name,ctypes.c_uint64)fornamein('total_physical','available_physical','total_page_file','available_page_file','total_virtual','available_virtual','extended')]status=MemoryStatus()status.length=ctypes.sizeof(status)ifnotctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)):raiseOSError('Cannot check Windows memory before loading FluxRT')returnstatus.available_page_filedefprocess_commit():"""Read memory already committed by this worker, including runtime libraries."""ifos.name!='nt':return0importctypesclassCounters(ctypes.Structure):_fields_=[('size',ctypes.c_uint32),('faults',ctypes.c_uint32)]+[(name,ctypes.c_size_t)fornamein('peak_working','working','peak_paged','paged','peak_nonpaged','nonpaged','page_file','peak_page_file','private')]counters=Counters()counters.size=ctypes.sizeof(counters)kernel=ctypes.WinDLL('kernel32',use_last_error=True)kernel.GetCurrentProcess.restype=ctypes.c_void_pread=ctypes.WinDLL('psapi',use_last_error=True).GetProcessMemoryInforead.argtypes=[ctypes.c_void_p,ctypes.POINTER(Counters),ctypes.c_uint32]ifnotread(kernel.GetCurrentProcess(),ctypes.byref(counters),counters.size):raisectypes.WinError(ctypes.get_last_error())returncounters.privateclassControls(NamedTuple):steps:intguidance:floatsource:floatstyle:floatwidth:intheight:intclassSettings(NamedTuple):prompt:strseed:intreference:np.ndarray|Nonetransition:strduration:floatframes:intcontrols:Controlsconsistency:floatalign_motion:boolreset:intprompt_b:strmix:floatsmooth:floatreference_enabled:boolreference_role:strcut:intoutput_fps:floatclassFeedback:"""Retain only the source and generated image from the last inference."""def__init__(self):self.source=self.output=Nonedefclear(self):self.source=self.output=Nonedefprepare(self,source,strength,align_motion):ifnotstrengthorself.sourceisNone:returnsource,0.0previous=self.outputweight=np.full(source.shape[:2],strength,dtype=np.float32)ifalign_motion:h,w=source.shape[:2]scale=min(1.0,256/max(h,w))size=(round(w*scale),round(h*scale))current=cv2.resize(cv2.cvtColor(source,cv2.COLOR_RGB2GRAY),size)old=cv2.resize(cv2.cvtColor(self.source,cv2.COLOR_RGB2GRAY),size)backward=cv2.calcOpticalFlowFarneback(current,old,None,.5,3,21,3,5,1.2,0)forward=cv2.calcOpticalFlowFarneback(old,current,None,.5,3,21,3,5,1.2,0)y,x=np.mgrid[:size[1],:size[0]].astype(np.float32)mx,my=x+backward[...,0],y+backward[...,1]warped_old=cv2.remap(old,mx,my,cv2.INTER_LINEAR)returned=cv2.remap(forward,mx,my,cv2.INTER_LINEAR)gray=current.astype(np.float32)variance=cv2.blur(gray*gray,(7,7))-cv2.blur(gray,(7,7))**2valid=((mx>=0)&(mx<size[0]-1)&(my>=0)&(my<size[1]-1)&(np.linalg.norm(backward+returned,axis=2)<1.5)&(np.abs(warped_old.astype(np.float32)-gray)<18)&(variance>4))confidence=cv2.GaussianBlur(valid.astype(np.float32),(5,5),0)*validweight*=cv2.resize(confidence,(w,h),interpolation=cv2.INTER_NEAREST)flow=cv2.resize(backward,(w,h))flow[...,0]*=w/size[0]flow[...,1]*=h/size[1]y,x=np.mgrid[:h,:w].astype(np.float32)previous=cv2.remap(previous,x+flow[...,0],y+flow[...,1],cv2.INTER_LINEAR)mixed=source.astype(np.float32)*(1-weight[...,None])+previous*weight[...,None]returnnp.ascontiguousarray(np.clip(np.rint(mixed),0,255),dtype=np.uint8),float(weight.mean()/strength)defremember(self,source,output):self.source,self.output=source,outputclassInference(ModelInferenceSubprocess):"""Use the pinned model pipeline inside goofi's existing Python worker."""def__init__(self,config):self.config=configself.resolution=config['resolution']self.height,self.width=self.resolution['height'],self.resolution['width']self.interpolation_exp=config['interpolation_exp']self.logging=Falsedefinit_shared_tensors(self):"""The node owns the frame handoff; no additional processes are needed."""
@staticmethoddefrelease():importgcimporttorchgc.collect()torch.cuda.empty_cache()defprocess_init(self):super().process_init()self.controls=Noneself.negative_prompt_embeds=Noneself.attention_bias=Noneself.reference_primary=Falsedefset_reference(self,enabled,primary):ifenabled!=self.config['use_reference_image']orprimary!=self.reference_primary:self.config['use_reference_image']=enabledself.reference_primary=primaryself.controls=Nonedefnoise_for_seed(self,seed):"""Make the unpacked noise accepted by the pinned pipeline."""importtorchscale=self.pipe.vae_scale_factor*2shape=(1,self.pipe.transformer.config.in_channels,self.height//scale,self.width//scale)returntorch.randn(shape,generator=torch.Generator(device=self.device).manual_seed(seed),device=self.device,dtype=self.prompt_embeds.dtype)
@staticmethoddefblend_noise(source,target,progress):"""Interpolate noise directions without reducing their magnitude."""importtorcha,b=source.float(),target.float()na,nb=a.norm(),b.norm()cosine=((a*b).sum()/(na*nb).clamp_min(1e-12)).clamp(-1,1)angle=torch.acos(cosine)ifabs(cosine.item())>0.9995:returntorch.lerp(a,b,progress).to(source.dtype)direction=(torch.sin((1-progress)*angle)*a/na+torch.sin(progress*angle)*b/nb)/torch.sin(angle)return(direction*((1-progress)*na+progress*nb)).to(source.dtype)defconfigure(self,controls):"""Apply model controls and invalidate only the affected caches."""if(controls.width,controls.height)!=(self.width,self.height):raiseValueError('Restart FluxRT after changing source/width or source/height')ifcontrols==self.controls:returnimporttorchfromfluxrt.stream_processor.update_controllerimportUpdateControllerprevious=self.controlsifpreviousisNoneorcontrols.steps!=previous.steps:self.pipe._attention_kwargs=Noneself.pipe.spatial_cache.clear()self.config['enable_spatial_cache']=controls.steps<=4self.update_controller=UpdateController(self.config,self.height,self.width,compression_ratio=16,reference_image_seq_len=256ifself.config['use_reference_image']elseNone)self.pipe.update_controller=self.update_controllerself.process_state['steps']=controls.stepsself.attention_bias=Noneifcontrols.source!=1or(self.config['use_reference_image']andcontrols.style!=1):text=self.prompt_embeds.shape[1]image=self.width*self.height//256reference=256ifself.config['use_reference_image']else0self.attention_bias=torch.zeros(1,1,1,text+2*image+reference,device=self.device,dtype=self.dtype)first=controls.styleifself.reference_primaryandreferenceelsecontrols.sourcesecond=controls.sourceifself.reference_primaryelsecontrols.styleself.attention_bias[...,text+image:text+2*image]=math.log(first)iffirstelse-torch.infifreference:self.attention_bias[...,text+2*image:]=math.log(second)ifsecondelse-torch.infifcontrols.guidance>1andself.negative_prompt_embedsisNone:withtorch.no_grad():self.negative_prompt_embeds,_=self.pipe.encode_prompt(prompt='',device=self.device,num_images_per_prompt=1,max_sequence_length=512,text_encoder_out_layers=(9,18,27))self.update_controller.reset_cache()self.controls=controlsdefprocess_frame_with_pipeline(self,frame):"""Generate with independent source and reference attention weights."""importtorchimages=[Image.fromarray(frame)]ifself.config['use_reference_image']:reference=np.asarray(self.reference_image)ifself.reference_primary:images=[Image.fromarray(resize_image(reference,self.width,self.height)),Image.fromarray(resize_image(frame,256,256))]else:images.append(Image.fromarray(resize_image(reference,256,256)))result=self.pipe(prompt_embeds=self.prompt_embeds,negative_prompt_embeds=self.negative_prompt_embeds,image=images,height=self.height,width=self.width,guidance_scale=self.controls.guidance,num_inference_steps=self.controls.steps,latents=self.noise.clone(),generator=torch.Generator(device=self.device).manual_seed(self.process_state['seed']),attention_kwargs={'attention_mask':self.attention_bias}ifself.attention_biasisnotNoneelseNone,output_type='np').images[0]return(np.clip(result,0,1)*255).astype(np.uint8)classRuntime:"""One model owner and a bounded handoff to one interpolation clock."""def__init__(self,config,image,settings):self.cv=threading.Condition()self.closed=Falseself.paused=Falseself.image,self.settings=image,settingsself.pending=Noneself.output=Noneself.error=Noneself.status='Loading'self.config=configself.infer=threading.Thread(target=self.generate,daemon=True,name='fluxrt-inference')self.present=threading.Thread(target=self.play,daemon=True,name='fluxrt-output')self.infer.start()self.present.start()defsubmit(self,image,settings):withself.cv:ifimageisnotNone:self.image=imageself.settings=settingsdeftake(self):withself.cv:output,self.output=self.output,Nonereturnoutput,self.status,self.errordefpause(self,paused):withself.cv:self.paused=pausedifpaused:self.pending=self.output=Noneself.cv.notify_all()defrequest_stop(self):withself.cv:self.closed=Trueself.pending=self.output=Noneself.cv.notify_all()defgenerate(self):try:withself.cv:whilenotself.closed:available=available_commit()required=max(2.0,self.config['memory_budget']-process_commit()/1024**3)ifnotself.pausedand(availableisNoneoravailable>=required*1024**3):breakself.status=('Paused — model not loaded'ifself.pausedelsef'Waiting for memory — {available/1024**3:.1f} GiB available; 'f'{required:.1f} GiB additional headroom estimated. Retrying automatically.')self.cv.wait(timeout=1.0)ifself.closed:returnself.status='Loading'model=Inference(self.config)model.process_init()applied=self.settings._replace(prompt=self.config['default_prompt'],prompt_b='',seed=self.config['default_seed'],reference=None)feedback=Feedback()current=target=model.prompt_embedsembeds_a=embeds_b=currentsmoothed=Nonehas_generated=Falselast_control_time=time.monotonic()noise=target_noise=model.noise_for_seed(applied.seed)noise_from=Nonecurrent_style=target_style=style_from=Noneblend_from=Noneblend_start=0.0progress=1.0whileTrue:withself.cv:ifself.pausedandnotself.closed:pause_start=time.monotonic()self.cv.wait_for(lambda:self.closedornotself.paused)blend_start+=time.monotonic()-pause_startlast_control_time=time.monotonic()ifself.closed:returnimage,settings=self.image,self.settingsstart=time.monotonic()force_cut=settings.cut!=applied.cutgoals=np.array([settings.controls.guidance,settings.controls.source,settings.controls.styleifsettings.reference_enabledelse0.0,settings.mix,settings.consistency],dtype=np.float64)previous_mix=NoneifsmoothedisNoneelsefloat(smoothed[3])previous_consistency=0.0ifsmoothedisNoneelsefloat(smoothed[4])alpha=1.0ifsettings.smooth==0orforce_cutelse1-math.exp(-3*(start-last_control_time)/settings.smooth)smoothed=goals.copy()ifsmoothedisNoneelsesmoothed+(goals-smoothed)*alphasmoothed[np.abs(goals-smoothed)<1e-4]=goals[np.abs(goals-smoothed)<1e-4]last_control_time=startcontrols=settings.controls._replace(guidance=float(smoothed[0]),source=float(smoothed[1]),style=float(smoothed[2]))active_reference=settings.referenceisnotNoneand(settings.reference_enabledorcontrols.style>0)model.set_reference(active_reference,active_referenceandsettings.reference_role=='reference')model.configure(controls)mix,consistency=float(smoothed[3]),float(smoothed[4])ifprevious_mix!=mix:model.update_controller.reset_cache()previous_progress=progressencode_seconds=0.0prompt_changed=settings.prompt!=applied.promptorsettings.prompt_b!=applied.prompt_bseed_changed=settings.seed!=applied.seedstyle_changed=settings.referenceisnotapplied.referencereset_feedback=(force_cutorsettings.reset!=applied.resetor(notconsistencyandprevious_consistency)or((prompt_changedorseed_changedorstyle_changed)and(settings.transition=='cut'orsettings.duration<=0)))ifreset_feedback:feedback.clear()model.previous_frame=Nonemodel.update_controller.reset_cache()elifsettings.reference_role!=applied.reference_role:feedback.clear()ifprompt_changed:ifsettings.prompt!=applied.prompt:model.update_prompt_embeds(settings.prompt)embeds_a=model.prompt_embedsifsettings.prompt_bandsettings.prompt_b!=applied.prompt_b:model.update_prompt_embeds(settings.prompt_b)embeds_b=model.prompt_embedsencode_seconds=time.monotonic()-starttarget=embeds_aifnotsettings.prompt_belseembeds_a*(1-mix)+embeds_b*mixifseed_changed:target_noise=model.noise_for_seed(settings.seed)ifstyle_changedandsettings.referenceisnotNone:target_style=settings.reference.astype(np.float32)ifcurrent_styleisNone:current_style=target_styleifprompt_changedorseed_changedorstyle_changed:blend_from=currentnoise_from=noisestyle_from=current_styleblend_start=time.monotonic()previous_progress=0.0elifblend_fromisnotNoneandsettings.duration!=applied.duration:blend_from=currentnoise_from=noisestyle_from=current_styleblend_start=time.monotonic()previous_progress=0.0ifblend_fromisnotNone:ifforce_cutorsettings.transition=='cut'orsettings.duration<=0ornothas_generated:progress=previous_progress=1.0model.previous_frame=Noneelse:progress=min(1.0,(time.monotonic()-blend_start)/settings.duration)current=targetifprogress==1.0elseblend_from*(1-progress)+target*progressnoise=target_noiseifprogress==1.0elsemodel.blend_noise(noise_from,target_noise,progress)iftarget_styleisnotNone:current_style=target_styleifprogress==1.0elsestyle_from*(1-progress)+target_style*progressmodel.reference_image=Image.fromarray(np.rint(current_style).astype(np.uint8))model.prompt_embeds=currentmodel.update_controller.reset_cache()ifprogress==1.0:blend_from=Nonenoise_from=Nonestyle_from=Noneelse:current=targetmodel.prompt_embeds=currentifforce_cut:current=targetnoise=target_noisemodel.prompt_embeds=currentiftarget_styleisnotNone:current_style=target_stylemodel.reference_image=Image.fromarray(np.rint(current_style).astype(np.uint8))blend_from=Noneprogress=previous_progress=1.0ifmodel.previous_frameisNone:previous_progress=progressmodel.process_state['seed']=settings.seedmodel.noise=noisemodel.interpolation_exp=2ifsettings.frames==4else3applied=settingssource=resize_image(image,settings.controls.width,settings.controls.height)conditioned,accepted=feedback.prepare(source,consistency,settings.align_motion)ifconsistency:# Feedback must reach inference even when the raw source is unchanged.model.update_controller.reset_cache()frame=model.process_frame_with_pipeline(conditioned)has_generated=Trueifconsistency:feedback.remember(source,frame)ifsettings.frames==1:frames=frame[None,...,::-1]model.previous_frame=Noneelse:frames=model.interpolate_frames(model.convert_np_to_torch(frame))elapsed=time.monotonic()-startwithself.cv:ifself.closed:returnifself.paused:continueself.pending=(frames,elapsed,previous_progress,{'prompt':settings.prompt,'seed':settings.seed,'blend_progress':progress,'batch_seconds':elapsed,'encode_seconds':encode_seconds,'rife_frames':settings.frames,'transition':settings.transition,'consistency':consistency,'feedback_coverage':accepted,'align_motion':settings.align_motion,'feedback_reset':settings.reset,'width':settings.controls.width,'height':settings.controls.height,'steps':controls.steps,'guidance':controls.guidance,'source_strength':controls.source,'style_strength':controls.style,'prompt_b':settings.prompt_b,'prompt_mix':mix,'reference_enabled':active_reference,'reference_role':settings.reference_role,'control_smoothing':settings.smooth,'cut':settings.cut,'spatial_cache':settings.controls.steps<=4,'generation_fps':1/elapsed})self.cv.notify_all()exceptExceptionaserror:withself.cv:self.error=f'{type(error).__name__}: {error}'self.cv.notify_all()finally:model=current=target=blend_from=Noneembeds_a=embeds_b=Nonenoise=target_noise=noise_from=Nonecurrent_style=target_style=style_from=NoneInference.release()defplay(self):previous=Noneprevious_cut=NonewhileTrue:withself.cv:self.cv.wait_for(lambda:self.closedorself.erroror(notself.pausedandself.pendingisnotNone))ifself.closedorself.error:returnframes,elapsed,previous_progress,metadata=self.pendingself.pending=Nonetarget_fps=self.settings.output_fpsstart=time.monotonic()# A batch remains private while it plays. The next pending batch can be replaced.count=max(1,math.ceil(elapsed*target_fps))iftarget_fpselselen(frames)interval=1/target_fpsiftarget_fpselseelapsed/countorigin=frames[0]ifpreviousisNoneorprevious_cut!=metadata['cut']elsepreviousprevious_cut=metadata['cut']foriinrange(count):withself.cv:whilenotself.closedandnotself.errorandnotself.paused:wait=start+(i+1)*interval-time.monotonic()ifwait<=0:breakself.cv.wait(wait)ifself.closedorself.error:returnifself.paused:previous=Nonebreakifi<count-1andtime.monotonic()>start+(i+2)*interval:continueposition=(i+1)*len(frames)/countright=min(len(frames)-1,math.ceil(position)-1)left=originifright==0elseframes[right-1]alpha=position-rightframe=frames[right]ifnottarget_fpselseleft.astype(np.float32)*(1-alpha)+frames[right]*alphargb=np.ascontiguousarray(frame[...,::-1],dtype=np.float32)/255.0progress=previous_progress+(metadata['blend_progress']-previous_progress)*(i+1)/countinfo={**metadata,'blend_progress':progress,'target_fps':target_fps,'playback':'rife + blend'iftarget_fpsandlen(frames)>1else'blend'iftarget_fpselse'rife'iflen(frames)>1else'direct'}phase='Ready'ifprogress==1.0elsef'Blending {progress:.0%}'playback='RIFE off'iflen(frames)==1elsef'RIFE x{len(frames)} | {len(frames)/elapsed:.1f} FPS playback target'iftarget_fps:playback=f"{info['playback']} | {target_fps:g} FPS target"self.status=(f'{1/elapsed:.2f} FPS generated (batch rate)\n'f"{metadata['width']} x {metadata['height']} | {metadata['steps']} steps\n"f'{phase} | {playback}')ifmetadata['consistency']:self.status+=(f"\nFeedback {metadata['consistency']:.0%} | "f"accepted {metadata['feedback_coverage']:.0%}")self.output=(rgb,{'channels':{'dim2':['r','g','b']},'fluxrt':info})previous=frames[-1]defstop(self):self.request_stop()self.present.join(timeout=1.0)self.infer.join(timeout=1.0)# goofi owns the subprocess. Its exit also releases a model still in a CUDA call.classFluxRT(goofi.Node):"""Edit a video or graphics stream. The 16 GB preset trades frame rate for memory."""TAGS=['image','ml','transform']INPUTS={'source':goofi.InputSlot(goofi.DataType.ARRAY,trigger=False,required=False),'reference':goofi.InputSlot(goofi.DataType.ARRAY,trigger=False,required=False)}OUTPUTS={'image':goofi.DataType.ARRAY,'status':goofi.DataType.STRING}PARAMS={'runtime':{'state':goofi.StringParam('stop',options=['start','pause','stop'],doc='Start loads and generates. Pause retains the model. Stop finishes active work and releases the model.'),'memory_budget':goofi.FloatParam(18.5,18.5,32.0,doc='Estimated total worker commit budget in GiB on Windows. Startup subtracts memory already held by this worker, with at least 2 GiB free required. Wait and retry if headroom is insufficient. Increase for more margin; set before Start.')},'model':{'root':goofi.StringParam('',doc='Absolute path to the prepared FluxRT checkout. Restart after changing it.'),'prompt':goofi.StringParam('Turn this into watercolor art.',doc='Editing instruction. Apply changes at an inference boundary.'),'prompt_b':goofi.StringParam('',doc='Second editing instruction. Empty uses prompt A at both ends. Encoded only when its text changes.'),'prompt_mix':goofi.FloatParam(0.0,0.0,1.0,doc='Blend prompt embeddings: 0 is prompt A, 1 is prompt B. Uses control smoothing; does not add a model pass.'),'seed':goofi.IntParam(52,0,2147483647,doc='Keep fixed for continuity. Changes blend noise over transition/duration, with RIFE between generated frames.'),'steps':goofi.IntParam(2,1,8,doc='Denoising steps. More steps cost time; above four, spatial caching is disabled to limit VRAM.'),'guidance':goofi.FloatParam(1.0,1.0,3.0,doc='Prompt guidance (CFG). Above one adds a second model pass per step and can exaggerate colors and detail.'),},'source':{'width':goofi.IntParam(320,256,512,options=[256,320,384,448,512],doc='Output width. Set before generation; restart the node to change size.'),'height':goofi.IntParam(320,256,512,options=[256,320,384,448,512],doc='Output height. Set before generation; restart the node to change size.'),'influence':goofi.FloatParam(1.0,0.0,2.0,doc='Source-image attention weight. Zero blocks source tokens, one is normal, two increases their influence. Not a denoising-strength percentage.'),'freeze':goofi.BoolParam(False,doc='Hold the current guide frame while controls remain live. Unfreeze accepts new input. Start while frozen captures the first frame.'),},'reference':{'enabled':goofi.BoolParam(False,doc='Enable the captured reference live. Disable fades its weight to zero, then removes its image tokens.'),'main_image':goofi.StringParam('source',options=['source','reference'],doc='Choose which image is the main one to transform. The other supplies additional conditioning. Update prompt instructions to match these roles. Changes apply between generations.'),'capture':goofi.PulseParam(doc='Capture the current reference input and blend the reference over transition/duration. Cut mode or zero duration applies it immediately.'),'influence':goofi.FloatParam(1.0,0.0,8.0,doc='Reference attention weight. One is normal. Values above two are experimental and can copy reference objects; this is not pure style transfer.')},'smoothing':{'time':goofi.FloatParam(0.25,0.0,5.0,doc='Seconds to approach 95% of a new CFG, image weight, reference weight, prompt mix or feedback target. Zero is immediate. New targets do not queue.'),'output_fps':goofi.FloatParam(0.0,0.0,60.0,doc='Target playback FPS. Zero delivers only the selected RIFE frames. Above zero blends between RIFE frames on a paced clock without extra model passes. Applies at the next batch; can soften detail or ghost motion. Does not increase generation FPS. Keep common/max_frequency above this target.')},'transition':{'mode':goofi.StringParam('prompt blend',options=['cut','prompt blend'],doc='Blend prompts, seed noise, and reference images with RIFE, or cut to the new settings.'),'duration':goofi.FloatParam(2.5,0.0,60.0,doc='Seconds for prompt, seed, and reference blends. Encoding, inference and playback add delay. Zero cuts. Changes restart the remaining blend from its current state.'),'frames':goofi.IntParam(4,1,8,options=[1,4,8],doc='1 disables RIFE; 4 or 8 adds interpolated output frames. More frames do not increase model generation speed.'),'cut':goofi.PulseParam(doc='Immediately apply all current targets and clear feedback and RIFE history at the next generation. Skip the current transition.'),},'consistency':{'strength':goofi.FloatParam(0.0,0.0,0.8,doc='Previous-output contribution to the source. Zero disables feedback. Start near 0.2; high values can cause drift or trails and reduce generation speed.'),'align_motion':goofi.BoolParam(True,doc='Warp feedback with source motion. Reject uncertain motion, changed regions, and exposed borders.'),'reset':goofi.PulseParam(doc='Clear feedback, model cache, and RIFE history at the next inference boundary.'),},'common':{'autotrigger':goofi.BoolParam(True),'max_frequency':goofi.FloatParam(60.0,1.0,120.0)},}defsetup(self):importfaulthandlerfaulthandler.enable()self.runtime=Noneself.root=Noneself.reference=Noneself.capture_style=Falseself.feedback_reset=0self.cut_generation=0self.output_times=deque()defpulse_reference_capture(self):self.capture_style=Truedefpulse_consistency_reset(self):self.feedback_reset+=1defpulse_transition_cut(self):self.cut_generation+=1defprocess(self,source=None,reference=None):image,style=source,referencestate=self.params.runtime.stateifstatenotin('start','pause','stop'):raiseValueError('Set runtime/state to start, pause, or stop')ifself.runtimeisnotNone:ifstate=='stop':self.runtime.request_stop()ifself.runtime.closed:ifself.runtime.infer.is_alive()orself.runtime.present.is_alive():return{'status':'Stopping — waiting for active model work'}self.runtime=Noneself.reference=self.root=Noneself.output_times.clear()elifself.runtime.error:raiseValueError(self.runtime.error)ifstate!='start':ifself.runtimeisnotNone:self.runtime.pause(True)return{'status':'Paused — model retained'ifself.runtimeisnotNoneelse'Stopped — select start to load'}root=self.params.model.rootifself.runtimeisnotNoneandroot!=self.root:raiseValueError('Restart FluxRT after changing model/root')enabled=self.params.reference.enabledifself.runtimeisnotNone:size=self.runtime.config['resolution']if(self.params.source.width,self.params.source.height)!=(size['width'],size['height']):raiseValueError('Restart FluxRT after changing source/width or source/height')ifenabledandstyleisnotNoneand(self.referenceisNoneorself.capture_style):self.reference=resize_image(self.prepare_image(style.data),self.params.source.width,self.params.source.height)self.capture_style=Falseifenabledandself.referenceisNone:return{'status':'Waiting for reference image'}settings=Settings(self.params.model.prompt,self.params.model.seed,self.reference,self.params.transition.mode,self.params.transition.duration,self.params.transition.frames,Controls(self.params.model.steps,self.params.model.guidance,self.params.source.influence,self.params.reference.influence,self.params.source.width,self.params.source.height),self.params.consistency.strength,self.params.consistency.align_motion,self.feedback_reset,self.params.model.prompt_b,self.params.model.prompt_mix,self.params.smoothing.time,enabled,self.params.reference.main_image,self.cut_generation,self.params.smoothing.output_fps)ifsettings.framesnotin(1,4,8):raiseValueError('Set transition/frames to 1, 4 or 8')ifsettings.reference_rolenotin('source','reference'):raiseValueError('Set reference/main_image to source or reference')ifsettings.transitionnotin('cut','prompt blend'):raiseValueError('Set transition/mode to cut or prompt blend')ifany(size<256orsize>512orsize%32forsizein(settings.controls.width,settings.controls.height)):raiseValueError('Set source/width and source/height to multiples of 32 between 256 and 512')incoming=NoneifimageisnotNoneand(self.runtimeisNoneornotself.params.source.freeze):incoming=self.prepare_image(image.data)ifself.runtimeisNone:ifincomingisNone:return{'status':'Waiting for image'}path=Path(root)ifnotrootornotpath.is_absolute():raiseValueError('Set model/root to the absolute path of the prepared FluxRT checkout')config=json.loads((path/'configs/benchmark_config.json').read_text())config.update(models_path=str(path/'FLUX.2-klein-4B'),memory_budget=self.params.runtime.memory_budget,int8_models_path=str(path/'FLUX.2-klein-4B-int8'),rife_path=str(path/'RIFE-safetensors/flownet.safetensors'),resolution={'height':settings.controls.height,'width':settings.controls.width},enable_int8_quantization=True,compile_models=False,enable_spatial_cache=settings.controls.steps<=4,interpolation_exp=2ifsettings.frames==4else3,enable_tiny_vae=False,enable_flow_upscaler=False,use_reference_image=enabled,reference_image_resolution={'height':256,'width':256},target_fps=None,logging=False,default_prompt=settings.prompt,default_seed=settings.seed,default_steps=settings.controls.steps)self.runtime=Runtime(config,incoming,settings)self.root=rootelse:self.runtime.submit(incoming,settings)self.runtime.pause(False)ifimageisnotNone:self.clear_input('source')output,status,error=self.runtime.take()iferror:raiseValueError(error)now=time.monotonic()ifoutputisnotNone:self.output_times.append(now)whileself.output_timesandself.output_times[0]<now-2:self.output_times.popleft()fps=len(self.output_times)/2.0result={'status':f'{fps:.1f} FPS output (delivered)\n{status}'}ifoutputisnotNone:data,meta=outputresult['image']=(data,{**meta,'fluxrt':{**meta['fluxrt'],'output_fps':fps}})returnresult
@staticmethoddefprepare_image(data,size=None):data=np.asarray(data)ifdata.ndim!=3ordata.shape[2]!=3ornotdata.sizeornotnp.isfinite(data).all():raiseValueError('FluxRT needs a finite RGB image with shape [height, width, 3]')ifdata.min()<0ordata.max()>1:raiseValueError('FluxRT needs RGB values in 0..1')data=np.ascontiguousarray(data*255.0,dtype=np.uint8)returnresize_image(data,size,size)ifsizeisnotNoneelsedatadefstop(self):ifself.runtimeisnotNone:self.runtime.stop()defresize_image(image,width,height):"""Center-crop the retained source to the output aspect ratio."""h,w=image.shape[:2]crop_w,crop_h=min(w,max(1,round(h*width/height))),min(h,max(1,round(w*height/width)))crop=image[(h-crop_h)//2:(h-crop_h)//2+crop_h,(w-crop_w)//2:(w-crop_w)//2+crop_w]returnnp.ascontiguousarray(cv2.resize(crop,(width,height)))