"""Generic ComfyUI/Irodori app backend supplied by the HF repository. The fixed v53 image executes this code as uid10001. HTML is hosted separately. Only a job-scoped Cloudflare capability is received; no provider keys are needed. """ from __future__ import annotations import base64,hashlib,json,math,mimetypes,os,re,shutil,subprocess,sys,time,urllib.error,urllib.parse,urllib.request,uuid from pathlib import Path TRUSTED_ORIGIN=os.environ['APP_SERVICE_ORIGIN'] USER_AGENT='app-HF/1.0' INPUT_ROOT=Path('/comfyui/input') OUTPUT_ROOT=Path('/comfyui/output') TEMP_ROOT=Path('/comfyui/temp') CHUNK_BYTES=8*1024*1024 MAX_ARTIFACT=512*1024*1024 MAX_TOTAL=2*1024*1024*1024 MAX_FILES=16 MEDIA_MARKERS=('loadimage','loadvideo','loadaudio') MEDIA_KEYS={'image','images','imagefile','imagepath','video','videos','videofile','videopath','audio','audiofile','audiopath','file','filename','path','filepath','directory','folder','url','uri'} MODEL_KEYS={'ckpt_name','unet_name','lora_name','vae_name','clip_name','clip_name1','clip_name2','clip_name3','clip_name4','control_net_name','model_name','text_encoder','text_encoder_name','tiny_vae','mmaudio_model','clip_model','vae_model','synchformer_model'} DENIED_MARKERS={'python','shell','terminal','subprocess','executecommand','systemcommand','commandline','comfyinstall','comfymanager'} MIMES={'.mp4':'video/mp4','.webm':'video/webm','.mov':'video/quicktime','.mkv':'video/x-matroska','.avi':'video/x-msvideo','.m4v':'video/mp4','.wav':'audio/wav','.flac':'audio/flac','.mp3':'audio/mpeg','.ogg':'audio/ogg','.opus':'audio/ogg','.m4a':'audio/mp4','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.gif':'image/gif','.avif':'image/avif'} class Deadline: def __init__(self,seconds=280):self.end=time.monotonic()+seconds def remaining(self,limit=None): remaining=self.end-time.monotonic() if remaining<=0:raise TimeoutError('Generation and artifact transfer exceeded the 280-second app limit') return min(remaining,limit) if limit is not None else remaining class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self,*args,**kwargs):raise ValueError('Media service redirects are not accepted') def trusted_url(value): if not isinstance(value,str) or len(value)>4096:raise ValueError('Invalid media service URL') parsed=urllib.parse.urlsplit(value) if parsed.scheme!='https' or parsed.netloc!=urllib.parse.urlsplit(TRUSTED_ORIGIN).netloc or parsed.username or parsed.password or parsed.fragment: raise ValueError('Media URL must use the trusted Cloudflare service origin') return value def set_read_timeout(response,deadline): remaining=deadline.remaining(25) # urllib's read timeout is otherwise reset per read, allowing a slow peer to # retain the original timeout after the overall job budget has elapsed. sock=getattr(getattr(getattr(response,'fp',None),'raw',None),'_sock',None) if sock is not None:sock.settimeout(remaining) def service_error(label,error,deadline): details=['HTTP '+str(error.code)] # Never reflect capability URLs, HTML, echoed request data, or credentials. try: set_read_timeout(error,deadline) snippet=error.read(8192).decode('utf-8',errors='replace') match=re.search(r'(?:error[ _-]*code|cloudflare[ _-]*error)\s*[:=]\s*["\']?(1[0-9]{3})\b',snippet,re.I) if match:details.append('Cloudflare '+match.group(1)) if error.headers.get('cf-mitigated')=='challenge':details.append('Cloudflare challenge') ray=error.headers.get('cf-ray','') if re.fullmatch(r'[0-9a-fA-F]{8,32}-[A-Z]{3}',ray):details.append('Ray '+ray) except Exception:pass return RuntimeError(label+' ('+', '.join(details)+')') def trusted_network_test(url,deadline): trusted_url(url) request=urllib.request.Request(url,headers={'User-Agent':USER_AGENT,'Range':'bytes=0-0','Accept':'application/octet-stream'}) try: with urllib.request.build_opener(NoRedirect()).open(request,timeout=deadline.remaining(25)) as response: set_read_timeout(response,deadline);received=len(response.read(1)) return {'status':response.status,'sample_bytes':received,'content_type':response.headers.get('Content-Type'),'content_length':response.headers.get('Content-Length'),'cache_status':response.headers.get('CF-Cache-Status')} except urllib.error.HTTPError as error:raise service_error('Network diagnostic failed',error,deadline) from None except urllib.error.URLError:raise RuntimeError('Network diagnostic connection failed') from None def request_json(url,method,body,deadline,headers=None): trusted_url(url) raw=body if isinstance(body,bytes) else json.dumps(body,separators=(',',':'),allow_nan=False).encode() actual={'User-Agent':USER_AGENT,'Content-Type':'application/octet-stream' if isinstance(body,bytes) else 'application/json',**(headers or {})} req=urllib.request.Request(url,data=raw,method=method,headers=actual) try: with urllib.request.build_opener(NoRedirect()).open(req,timeout=deadline.remaining(25)) as response: set_read_timeout(response,deadline) encoded=response.read(65537) if len(encoded)>65536:raise ValueError('Media service JSON response is too large') result=json.loads(encoded) if not isinstance(result,dict):raise ValueError('Invalid media service response') return result except urllib.error.HTTPError as error: # Capability URLs must never appear in error messages returned to the user. raise service_error('Media service request failed',error,deadline) from None except urllib.error.URLError as error: raise RuntimeError('Media service connection failed: '+str(type(error.reason).__name__)) from None def relative_name(value): return isinstance(value,str) and bool(value) and len(value)<=512 and not value.startswith('/') and '\\' not in value and ':' not in value and not any(part in ('','..','.') for part in value.split('/')) and '\x00' not in value def reference_name(value): if not isinstance(value,str):return None if value.startswith('input/'):value=value[6:] if value.endswith(' [input]'):value=value[:-8] return value if relative_name(value) else None def safe_cache_path(root,value,directory=False): if not relative_name(value):raise ValueError('Invalid cached model path') source=Path(root)/value # Native HF snapshot files are symlinks into this repository's blob directory. resolved=source.resolve();repo=Path(root).parent.parent.resolve() if not (resolved.is_relative_to(Path(root).resolve()) or resolved.is_relative_to(repo)): raise ValueError('Cached model escaped its repository') if not (source.is_dir() if directory else source.is_file()):raise ValueError('Required file is missing from the native HF cache: '+value) return source def prepare_files(files,job,deadline): if files is None:files=[] if not isinstance(files,list) or len(files)>20:raise ValueError('files must contain at most20 items') directory=INPUT_ROOT/'hf-jobs'/job directory.mkdir(parents=True,exist_ok=False) references={};total=0 try: for item in files: deadline.remaining() if not isinstance(item,dict):raise ValueError('Invalid input file') name=item.get('name') if not isinstance(name,str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,159}',name) or name in references: raise ValueError('Input file names must be unique safe basenames') if Path(name).suffix.lower() not in MIMES:raise ValueError('Unsupported input media extension: '+name) expected=item.get('size');expected_hash=item.get('sha256') if expected is not None and (isinstance(expected,bool) or not isinstance(expected,int) or expected<1 or expected>MAX_ARTIFACT): raise ValueError('Input file size is invalid or exceeds512MiB') if expected_hash is not None and not re.fullmatch(r'[a-f0-9]{64}',str(expected_hash)): raise ValueError('Input SHA256 must be lowercase hexadecimal') target=directory/(uuid.uuid4().hex+Path(name).suffix.lower()) digest=hashlib.sha256();size=0 with target.open('xb') as out: if item.get('url'): trusted_url(item['url']) if expected is None or expected_hash is None:raise ValueError('Staged inputs require size and sha256') try: request=urllib.request.Request(item['url'],headers={'User-Agent':USER_AGENT,'Accept':'application/octet-stream'}) response=urllib.request.build_opener(NoRedirect()).open(request,timeout=deadline.remaining(25)) except urllib.error.HTTPError as error:raise service_error('Input media download failed',error,deadline) from None except urllib.error.URLError as error:raise RuntimeError('Input media download connection failed') from None with response: while True: deadline.remaining() set_read_timeout(response,deadline) chunk=response.read(256*1024) if not chunk:break size+=len(chunk);total+=len(chunk) if size>expected or size>MAX_ARTIFACT or total>MAX_TOTAL:raise ValueError('Input media size exceeds declared or job limits') digest.update(chunk);out.write(chunk) elif isinstance(item.get('data'),str): encoded=item['data'].split(',',1)[-1] if len(encoded)>768*1024:raise ValueError('Inline media is too large; stage it through Cloudflare first') try:raw=base64.b64decode(encoded,validate=True) except ValueError:raise ValueError('Invalid input base64') from None size=len(raw);total+=size;digest.update(raw);out.write(raw) else:raise ValueError('Input media requires a staged URL') if not size or (expected is not None and size!=expected):raise ValueError('Input media size does not match its manifest') if expected_hash is not None and digest.hexdigest()!=expected_hash:raise ValueError('Input media SHA256 does not match its manifest') references[name]=target.relative_to(INPUT_ROOT).as_posix() return references,directory except BaseException: shutil.rmtree(directory,ignore_errors=True) raise def normalize_and_validate(workflow,object_info,references,job): if not isinstance(workflow,dict) or not workflow or len(workflow)>512:raise ValueError('workflow must be a non-empty API graph with at most512 nodes') workflow=json.loads(json.dumps(workflow,allow_nan=False)) for node_id,node in workflow.items(): if not isinstance(node_id,str) or not re.fullmatch(r'[A-Za-z0-9_-]{1,80}',node_id):raise ValueError('Invalid workflow node ID') if not isinstance(node,dict) or not isinstance(node.get('inputs'),dict):raise ValueError('Invalid workflow node: '+node_id) node_type=node.get('class_type') if not isinstance(node_type,str) or node_type not in object_info:raise ValueError('Node type is not installed: '+str(node_type)) normalized=re.sub('[^a-z0-9]','',node_type.lower()) if any(marker in normalized for marker in DENIED_MARKERS):raise ValueError('This node cannot run in the shared runtime: '+node_type) definitions=object_info[node_type].get('input',{}) definitions={**definitions.get('required',{}),**definitions.get('optional',{})} if object_info[node_type].get('output_node') and 'filename_prefix' in definitions and 'filename_prefix' not in node['inputs']: node['inputs']['filename_prefix']='output' for key,value in list(node['inputs'].items()): lower=str(key).lower() if isinstance(value,str) and len(value)>20000:raise ValueError('Workflow text is too long') if isinstance(value,list) and len(value)==2 and isinstance(value[0],str) and isinstance(value[1],int): if value[0] not in workflow or not 0<=value[1]<=128:raise ValueError('Broken workflow link in node '+node_id) if isinstance(value,(int,float)) and not isinstance(value,bool): if not math.isfinite(value):raise ValueError('Workflow numbers must be finite') limits={'width':8192,'height':8192,'target_width':8192,'target_height':8192,'output_width':8192,'output_height':8192, 'length':2000,'num_frames':2000,'frames':2000,'video_frames':2000,'frame_count':2000, 'fps':240,'frame_rate':240,'steps':120,'batch':8,'batch_size':8,'batch_count':8,'duration':300,'duration_seconds':300} if lower in limits and not 0<=value<=limits[lower]:raise ValueError('Workflow exceeds '+lower+' limit') if (lower in MODEL_KEYS or re.fullmatch(r'lora_\d+',lower)) and isinstance(value,str) and value.lower() not in ('none','disabled'): if not relative_name(value):raise ValueError('Unsafe model path in '+node_id) definition=definitions.get(key) options=definition[0] if isinstance(definition,list) and definition and isinstance(definition[0],list) else None if options and value not in options: basename=Path(value).name matches=[option for option in options if isinstance(option,str) and Path(option).name==basename] if len(matches)==1:node['inputs'][key]=matches[0] else:raise ValueError('Model is missing from the native HF cache mapping: '+value) if lower in ('filename_prefix','subfolder') and isinstance(value,str): if value and not relative_name(value):raise ValueError('Unsafe output path') node['inputs'][key]='hf-jobs/'+job+'/'+(value or 'output') media_key=re.sub('[^a-z0-9]','',lower) if any(marker in normalized for marker in MEDIA_MARKERS) and media_key in MEDIA_KEYS and isinstance(value,str): source=reference_name(value) if source not in references:raise ValueError('Workflow input media was not supplied by this job: '+str(source)) mapped=references[source] if node_type=='VHS_LoadVideoPath' and media_key=='video': # Path loaders use the path directly, unlike input-folder loaders. target=(INPUT_ROOT/mapped).resolve() job_root=(INPUT_ROOT/'hf-jobs'/job).resolve() if not target.is_relative_to(job_root) or not target.is_file(): raise ValueError('Video path must reference an existing input from this job') mapped=str(target) node['inputs'][key]=mapped return workflow def snapshot_outputs(): state={} for root in (OUTPUT_ROOT,TEMP_ROOT): if not root.exists():continue for path in root.rglob('*'): try: resolved=path.resolve() if path.is_file() and resolved.is_relative_to(root.resolve()): stat=path.stat();state[resolved]=(stat.st_size,stat.st_mtime_ns) except OSError:pass return state def output_files(history,before): paths=[];owned=[];seen=set() def visit(value,depth=0): if depth>8:return if isinstance(value,list): for child in value:visit(child,depth+1) elif isinstance(value,dict): if isinstance(value.get('filename'),str): name=value['filename'];subfolder=value.get('subfolder','');kind=value.get('type','output') roots={'output':OUTPUT_ROOT,'temp':TEMP_ROOT} if kind not in roots or not relative_name(name) or (subfolder and not relative_name(subfolder)):return root=roots[kind].resolve();path=(root/subfolder/name).resolve() if not path.is_relative_to(root) or not path.is_file() or path in seen:return seen.add(path);paths.append(path) stat=path.stat() if before.get(path)!=(stat.st_size,stat.st_mtime_ns):owned.append(path) else: for child in value.values():visit(child,depth+1) visit(history.get('outputs',{})) if not paths:raise RuntimeError('ComfyUI completed without an image, video or audio artifact') if len(paths)>MAX_FILES:raise ValueError('Workflow produced more than16 output artifacts') return paths,owned def comfy_execution_ms(history): """Use ComfyUI execution lifecycle timestamps; exclude input/output transfer.""" events={} for item in (history.get('status') or {}).get('messages',[]): if not isinstance(item,(list,tuple)) or len(item)!=2:continue name,data=item if name not in ('execution_start','execution_success') or not isinstance(data,dict):continue stamp=data.get('timestamp') if isinstance(stamp,(int,float)) and not isinstance(stamp,bool) and math.isfinite(stamp):events[name]=(data.get('prompt_id'),stamp) start=events.get('execution_start');end=events.get('execution_success') if not start or not end or not start[0] or start[0]!=end[0] or end[1]MAX_FILES or sum(path.stat().st_size for path in paths)>MAX_TOTAL:raise ValueError('Output artifacts exceed the per-job limit') if capability: trusted_url(capability) parsed=urllib.parse.urlsplit(capability) if parsed.query or not re.fullmatch(r'/a/[A-Za-z0-9_-]{43}',parsed.path):raise ValueError('Invalid job-scoped artifact capability') elif sum(path.stat().st_size for path in paths)>7*1024*1024: raise ValueError('A Cloudflare artifact upload capability is required for output larger than7MiB') outputs=[] for path in paths: metadata=file_metadata(path,deadline) if not capability: outputs.append({**metadata,'data':base64.b64encode(path.read_bytes()).decode()});continue response=request_json(capability+'/begin','POST',metadata,deadline) identifier=response.get('id') if not isinstance(identifier,str) or not re.fullmatch(r'[A-Za-z0-9_-]{8,100}',identifier):raise RuntimeError('Invalid artifact upload ID') parts=[] with path.open('rb') as source: index=0 while True: deadline.remaining();chunk=source.read(CHUNK_BYTES) if not chunk:break digest=hashlib.sha256(chunk).hexdigest() response=request_json(capability+'/'+identifier+'/parts/'+str(index),'PUT',chunk,deadline,{'X-Part-SHA256':digest}) if response.get('sha256') and response['sha256']!=digest:raise RuntimeError('Artifact part checksum mismatch') parts.append({'index':index,'sha256':digest,'size':len(chunk)});index+=1 completed=request_json(capability+'/'+identifier+'/complete','POST',{'parts':parts,'sha256':metadata['sha256'],'size':metadata['size']},deadline) url=completed.get('url');trusted_url(url) if completed.get('sha256') and completed['sha256']!=metadata['sha256']:raise RuntimeError('Uploaded artifact checksum mismatch') if completed.get('size') is not None and completed['size']!=metadata['size']:raise RuntimeError('Uploaded artifact size mismatch') outputs.append({**metadata,'url':url,'expires_at':completed.get('expires_at')}) return outputs def finite_number(body,key,default,minimum,maximum,integer=False): raw=body.get(key,default) if isinstance(raw,bool):raise ValueError(key+' must be numeric') try:value=float(raw) except (TypeError,ValueError):raise ValueError(key+' must be numeric') from None if not math.isfinite(value) or not minimum<=value<=maximum or (integer and value!=int(value)): raise ValueError(key+' must be between'+str(minimum)+' and'+str(maximum)) return int(value) if integer else value def run_irodori(payload,context,references,job,deadline): config=json.loads((context.root/'app-runtime.json').read_text(encoding='utf-8-sig')).get('irodori') if not isinstance(config,dict):raise ValueError('HF app is missing its irodori model configuration') text=payload.get('text');caption=payload.get('caption','') if not isinstance(text,str) or not text.strip() or len(text)>1000:raise ValueError('Irodori text must contain1..1000 characters') if not isinstance(caption,str) or len(caption)>2000:raise ValueError('Irodori caption exceeds2000 characters') checkpoint=safe_cache_path(context.root,config.get('checkpoint')) codec=safe_cache_path(context.root,config.get('codec')) tokenizer=safe_cache_path(context.root,config.get('tokenizer','tokenizer'),directory=True) if not (tokenizer/'tokenizer_config.json').is_file():raise ValueError('Irodori tokenizer_config.json is missing') directory=OUTPUT_ROOT/'hf-jobs'/job;directory.mkdir(parents=True,exist_ok=True) output=directory/'irodori.wav' reference=payload.get('reference_audio') many=payload.get('reference_audios') if reference is not None and many is not None:raise ValueError('Use one reference representation') selected=many if many is not None else ([reference] if reference else []) if not isinstance(selected,list) or len(selected)>20 or any(name not in references for name in selected):raise ValueError('Irodori reference audio was not supplied by this job') if sum((INPUT_ROOT/references[name]).stat().st_size for name in selected)>20*1024*1024:raise ValueError('Irodori reference audio exceeds20MiB combined') request={'checkpoint':str(checkpoint),'codec':str(codec),'tokenizer':str(tokenizer),'output':str(output), 'text':text.strip(),'caption':caption.strip(),'reference_wav':str(INPUT_ROOT/references[reference]) if reference else None, 'reference_wavs':[str(INPUT_ROOT/references[name]) for name in many] if many is not None else None, 'num_steps':finite_number(payload,'num_steps',60,4,80,True),'duration_scale':finite_number(payload,'duration_scale',1,.5,2), 'cfg_scale_text':finite_number(payload,'cfg_scale_text',3,0,20),'cfg_scale_caption':finite_number(payload,'cfg_scale_caption',3,0,20), 'cfg_scale_speaker':finite_number(payload,'cfg_scale_speaker',10,0,20), 'seed':None if payload.get('seed') is None else finite_number(payload,'seed',0,0,2**53-1,True), 'seconds':None if payload.get('seconds') is None else finite_number(payload,'seconds',5,.5,45)} runner=context.root/'irodori_runner.py' if not runner.is_file():raise ValueError('irodori_runner.py is missing from the HF app') log=directory/'irodori.stderr' with log.open('wb') as error_log: try: result=subprocess.run(['/opt/irodori-venv/bin/python',str(runner)],input=json.dumps(request),text=True, stdout=subprocess.PIPE,stderr=error_log,timeout=deadline.remaining(),check=False, env={**os.environ,'HF_HUB_OFFLINE':'1','TRANSFORMERS_OFFLINE':'1'}) except subprocess.TimeoutExpired:raise TimeoutError('Irodori exceeded the remaining280-second app budget') from None if result.returncode: with log.open('rb') as error_log:error_log.seek(max(0,log.stat().st_size-1600));detail=error_log.read().decode(errors='replace') raise RuntimeError('Irodori generation failed: '+detail) try:summary=json.loads(result.stdout) except ValueError:raise RuntimeError('Irodori returned invalid metadata') from None if summary.get('error'):raise RuntimeError(str(summary['error'])[:1600]) if not output.is_file():raise RuntimeError('Irodori returned no WAV file') return [output],summary def cleanup_job(directory,roots): if directory is None:return resolved=directory.resolve() if any(resolved.is_relative_to((root/'hf-jobs').resolve()) and resolved!=(root/'hf-jobs').resolve() for root in roots): shutil.rmtree(directory,ignore_errors=True) def elapsed_ms(started): return max(0,math.ceil((time.monotonic()-started)*1000)) def generate(payload,context): if not isinstance(payload,dict):raise ValueError('Generation payload must be an object') deadline=Deadline(280);started=time.monotonic();job=uuid.uuid4().hex metadata=payload.get('_app') or {} if not isinstance(metadata,dict):raise ValueError('Invalid artifact metadata') capability=metadata.get('artifact_upload_url') if capability:trusted_url(capability) references={};input_directory=None;owned=[];additional=[] try: references,input_directory=prepare_files(payload.get('files'),job,deadline) if payload.get('task')=='diagnostics': execution_started=time.monotonic() result=diagnostics(payload,context) result['summary']={'backend':'diagnostics','executed_ms':elapsed_ms(execution_started)} return result if payload.get('task')=='chat': execution_started=time.monotonic() result=run_chat(payload,context,deadline) result['summary']['executed_ms']=elapsed_ms(execution_started) result['summary']['elapsed_seconds']=round(time.monotonic()-started,3) return result if payload.get('task')=='irodori': execution_started=time.monotonic() paths,summary=run_irodori(payload,context,references,job,deadline);owned=paths summary['executed_ms']=elapsed_ms(execution_started) else: additional_ms=0 if payload.get('task')=='dialogue': settings=payload.get('irodori') if not isinstance(settings,dict):raise ValueError('Dialogue requires Irodori synthesis settings') execution_started=time.monotonic() audio,tts_summary=run_irodori(settings,context,references,job,deadline) additional_ms=elapsed_ms(execution_started) tts_summary['executed_ms']=additional_ms reference=input_directory/'dialogue-generated.wav' shutil.copyfile(audio[0],reference) references['dialogue-generated.wav']=reference.relative_to(INPUT_ROOT).as_posix() additional.extend(audio);owned.extend(audio) continuation=prepare_continuation(payload.get('continuation'),references,input_directory,deadline) workflow=payload.get('workflow') if workflow is None: default=context.root/'workflow.json' if not default.is_file():raise ValueError('Provide an API workflow or include workflow.json in the HF app') workflow=json.loads(default.read_text(encoding='utf-8-sig')) if payload.get('task')=='dialogue':workflow=dialogue_timing(workflow,tts_summary) info=context.comfy('/object_info') workflow=normalize_and_validate(workflow,info,references,job) before=snapshot_outputs();history=run_comfy(workflow,context,deadline) paths,new_outputs=output_files(history,before);owned.extend(new_outputs) paths,merged=finish_continuation(paths,continuation,job,deadline);owned.extend(merged) executed_ms=comfy_execution_ms(history) if executed_ms is None:executed_ms=history['_execution_ms'] paths.extend(additional);summary={'backend':'comfyui','nodes':len(workflow),'executed_ms':executed_ms+additional_ms} if additional:summary['irodori']=tts_summary if continuation:summary['continuation']=True outputs=upload_outputs(paths,capability,deadline) return {'files':outputs,'summary':{**summary,'elapsed_seconds':round(time.monotonic()-started,3)}} finally: cleanup_job(input_directory,(INPUT_ROOT,)) for path in owned: try: resolved=path.resolve() if any(resolved.is_relative_to(root.resolve()) for root in (OUTPUT_ROOT,TEMP_ROOT)):path.unlink(missing_ok=True) except OSError:pass for root in (OUTPUT_ROOT,TEMP_ROOT):cleanup_job(root/'hf-jobs'/job,(root,)) def run_chat(payload,context,deadline): config=json.loads((context.root/'app-runtime.json').read_text(encoding='utf-8-sig')).get('chat') or {} model_path=context.root if config.get('model_path','.')=='.' else safe_cache_path(context.root,config['model_path'],directory=True) if not (model_path/'config.json').is_file():raise ValueError('Chat model config.json is missing from the native HF cache') messages=payload.get('messages') if messages is None: messages=[] if payload.get('system_prompt'):messages.append({'role':'system','content':payload['system_prompt']}) if isinstance(payload.get('history'),list):messages.extend(payload['history']) messages.append({'role':'user','content':payload.get('message','')}) if not isinstance(messages,list) or not 1<=len(messages)<=64:raise ValueError('Chat requires1..64 messages') for message in messages: if not isinstance(message,dict) or message.get('role') not in ('system','user','assistant') or not isinstance(message.get('content'),str) or not message['content'].strip() or len(message['content'])>20000: raise ValueError('Chat messages require role and nonempty text content') request={'model_path':str(model_path),'messages':messages,'max_tokens':finite_number(payload,'max_tokens',512,1,2048,True), 'temperature':finite_number(payload,'temperature',.78,0,2),'top_p':finite_number(payload,'top_p',.9,.01,1), 'repetition_penalty':finite_number(payload,'repetition_penalty',1.08,1,2), 'seed':None if payload.get('seed') is None else finite_number(payload,'seed',0,0,2**53-1,True), 'generation_seconds':deadline.remaining()} directory=Path('/workspace/hf-app');directory.mkdir(parents=True,exist_ok=True) log=directory/('chat-'+uuid.uuid4().hex+'.stderr') try: with log.open('wb') as error_log: try:result=subprocess.run(['/opt/venv/bin/python',str(context.root/'chat_runner.py')],input=json.dumps(request),text=True, stdout=subprocess.PIPE,stderr=error_log,timeout=deadline.remaining(),check=False, env={**os.environ,'HF_HUB_OFFLINE':'1','TRANSFORMERS_OFFLINE':'1'}) except subprocess.TimeoutExpired:raise TimeoutError('Chat exceeded the remaining280-second app budget') from None try:output=json.loads(result.stdout) except ValueError:raise RuntimeError('Chat returned invalid JSON') from None if result.returncode or output.get('error'):raise RuntimeError('Chat failed: '+str(output.get('error','runtime exited'))[:1600]) return output finally:log.unlink(missing_ok=True) def prepare_continuation(value,references,directory,deadline): import bernini_continuation as module config=module.validate_continuation(value) if config is None:return None name=config['source_video_name'] if name not in references:raise ValueError('Continuation source video was not supplied') source=INPUT_ROOT/references[name] frame=directory/(uuid.uuid4().hex+'.png') resize=f"scale={config['width']}:{config['height']}:force_original_aspect_ratio=decrease,pad={config['width']}:{config['height']}:(ow-iw)/2:(oh-ih)/2:color=black,setsar=1" module._DEADLINE=deadline errors=[] for offset in ('-0.1','-0.5','-1.0'): frame.unlink(missing_ok=True) try:module._run(['ffmpeg','-hide_banner','-loglevel','error','-y','-sseof',offset,'-i',str(source),'-map','0:v:0','-an','-frames:v','1','-vf',resize,str(frame)],'continuation frame extraction',30) except (RuntimeError,subprocess.TimeoutExpired) as error:errors.append(str(error)) if frame.is_file() and frame.stat().st_size:break else: frame.unlink(missing_ok=True) module._run(['ffmpeg','-hide_banner','-loglevel','error','-y','-i',str(source),'-map','0:v:0','-an','-vf',resize,'-fps_mode','passthrough','-update','1',str(frame)],'continuation full-decode frame extraction',deadline.remaining()) if not frame.is_file() or not frame.stat().st_size:raise RuntimeError('Continuation produced no reference frame') references[config['last_frame_name']]=frame.relative_to(INPUT_ROOT).as_posix() return {**config,'source_path':source} def finish_continuation(paths,config,job,deadline): if config is None:return paths,[] import bernini_continuation as module module._DEADLINE=deadline outputs=[];created=[] for path in paths: if path.suffix.lower() not in module.VIDEO_SUFFIXES:outputs.append(path);continue target=OUTPUT_ROOT/'hf-jobs'/job/('continued-'+str(len(created))+'.mp4') module.merge_continuation_output(config['source_path'],path,target,config) outputs.append(target);created.append(target) if not created:raise RuntimeError('Continuation workflow produced no video') return outputs,created def diagnostics(payload,context): """Bounded, read-only inspection for version/mapping and workflow smoke checks.""" network=trusted_network_test(payload['network_url'],Deadline(25)) if payload.get('network_url') else None if payload.get('network_only'): if network is None:raise ValueError('network_only requires a trusted network_url') return {'network':network} requested=payload.get('requested_class_types',[]) if not isinstance(requested,list) or len(requested)>128 or any(not isinstance(name,str) or len(name)>160 for name in requested): raise ValueError('requested_class_types must contain at most128 node names') info=context.comfy('/object_info') result={'network':network,'available_node_count':len(info),'missing':[name for name in requested if name not in info], 'nodes':{}} for name in requested: if name not in info:continue node=info[name] result['nodes'][name]={'input':node.get('input',{}),'output':node.get('output',[]),'output_node':bool(node.get('output_node'))} stats=context.comfy('/system_stats') system=stats.get('system',{}) result['versions']={key:system.get(key) for key in ('comfyui_version','python_version','pytorch_version','embedded_python') if key in system} workflow=payload.get('workflow') if workflow is not None: references={} for item in payload.get('files',[]): if isinstance(item,dict) and isinstance(item.get('name'),str):references[item['name']]='hf-jobs/diagnostics/'+item['name'] normalized=normalize_and_validate(workflow,info,references,'diagnostics') result['workflow']={'valid':True,'nodes':len(normalized),'model_inputs':{node_id:{key:value for key,value in node['inputs'].items() if key.lower() in MODEL_KEYS or re.fullmatch(r'lora_\d+',key.lower())} for node_id,node in normalized.items()}} if len(json.dumps(result,separators=(',',':')).encode())>512*1024:raise ValueError('Requested diagnostic schema exceeds512KiB; request fewer nodes') return result def dialogue_timing(workflow,summary): """Preserve the host dialogue graph's speech-duration dependent frame count.""" if not isinstance(workflow,dict):raise ValueError('Dialogue workflow must be an object') workflow=json.loads(json.dumps(workflow,allow_nan=False)) expected={'796':'mxSlider','535':'LTXVEmptyLatentAudio','542':'PrimitiveFloat','549':'VHS_VideoCombine'} if any(workflow.get(key,{}).get('class_type')!=kind for key,kind in expected.items()): raise ValueError('Dialogue workflow is missing its duration/frame-rate controls') fps=finite_number(workflow['542']['inputs'],'value',24,1,120) duration=finite_number(summary,'duration_seconds',0,.001,45) seconds=max(3,math.ceil(duration+.25));frames=int(seconds*fps) workflow['796']['inputs'].update(Xi=frames,Xf=frames) workflow['535']['inputs'].update(frames_number=frames+1,frame_rate=fps) workflow['542']['inputs']['value']=fps workflow['549']['inputs']['frame_rate']=fps return workflow