diff --git a/SPIRIT_LEGACY_TRELLIS_STUDIO.md b/SPIRIT_LEGACY_TRELLIS_STUDIO.md new file mode 100644 index 00000000..92a349dc --- /dev/null +++ b/SPIRIT_LEGACY_TRELLIS_STUDIO.md @@ -0,0 +1,53 @@ +# Spirit Legacy TRELLIS Studio + +This adds a production-oriented browser GUI on top of the existing TRELLIS.2 ROCm fork without replacing `app.py`. + +## Features + +- Front, Back, Left, Right, Top, Bottom, Extra View 1, Extra View 2 file selectors. +- Source filenames do not matter; selected files are copied into the job folder and their original filenames are recorded. +- One image works as a single-image job. Two or more images use multi-image conditioning. +- Multi-image fusion modes: `multidiffusion` (averages all image predictions at each sampler step) and `stochastic` (cycles through images, lighter compute). +- Presets: Phase 8 Safe, Fast Preview, Game Asset, High-Res Master (Experimental), Ultra 1536 (Experimental), Custom. +- Advanced sampler, decimation, texture-size, token and seed controls. +- Every run saves `request.json`, `job.json`, `metadata.json`, `worker.log`, the normalized input copies, and `model.glb`. +- The generated GLB and metadata are returned through the browser. + +## ROCm / R9700 behavior + +The worker preserves the known-good Phase-8 environment: SDPA attention, `flex_gemm` sparse convolution, gfx1201, ROCm 7.14 paths, low-VRAM model movement, and releasing inference models before o-voxel GLB postprocessing. + +Input images must be RGBA PNG files with meaningful transparency. This deliberately avoids the BiRefNet path that previously failed on this ROCm setup. + +`Phase 8 Safe` is the verified 512 baseline. The 1024 and 1536 presets use the corresponding TRELLIS.2 cascade models but are marked experimental until validated on this exact R9700 installation. + +## Pull and run on the TRELLIS machine + +```bash +cd /home/foster/TRELLIS.2-ROCm +git pull origin rocm +chmod +x tools/start_spirit_legacy_gui.sh tools/spirit_legacy_worker.py tools/spirit_legacy_gui.py +./tools/start_spirit_legacy_gui.sh +``` + +Open: + +```text +http://100.125.111.71:7860 +``` + +## Windows launcher + +`Start-TRELLIS-Studio.bat` connects to `100.125.111.71` over SSH, starts the web GUI if it is not already running, and opens the browser. + +## Output layout + +```text +/home/foster/trellis2-outputs/studio//__/ +``` + +Each asset run retains its inputs and exact settings for later reproduction. + +## View semantics + +The view labels are retained for organization and reproducibility. The multi-image conditioning itself operates on the supplied image set, so TRELLIS does not require the source files to be named `front.png`, `back.png`, and so on. Top, Bottom, and extra 3/4-angle images can participate in the same fusion path. diff --git a/Start-TRELLIS-Studio.bat b/Start-TRELLIS-Studio.bat new file mode 100644 index 00000000..f454ec67 --- /dev/null +++ b/Start-TRELLIS-Studio.bat @@ -0,0 +1,18 @@ +@echo off +setlocal +title Spirit Legacy TRELLIS Studio +set HOST=100.125.111.71 +set USER=foster +set REPO=/home/foster/TRELLIS.2-ROCm +set PORT=7860 + +echo Starting Spirit Legacy TRELLIS Studio on %HOST%... +ssh %USER%@%HOST% "mkdir -p /home/foster/trellis2-outputs && if pgrep -f '[s]pirit_legacy_gui.py' >/dev/null; then echo TRELLIS Studio already running; else nohup %REPO%/tools/start_spirit_legacy_gui.sh >/home/foster/trellis2-outputs/trellis-studio.log 2>&1 nul +start "" "http://%HOST%:%PORT%" +endlocal diff --git a/tools/spirit_legacy_gui.py b/tools/spirit_legacy_gui.py new file mode 100755 index 00000000..89b3e6a0 --- /dev/null +++ b/tools/spirit_legacy_gui.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +from __future__ import annotations +import json,os,random,re,shutil,subprocess,time +from datetime import datetime +from pathlib import Path +import gradio as gr +from PIL import Image + +ROOT=Path(__file__).resolve().parents[1] +PYTHON=Path(os.environ.get('TRELLIS_PYTHON','/home/foster/trellis2-env/bin/python')) +WORKER=Path(os.environ.get('TRELLIS_STUDIO_WORKER',str(ROOT/'tools'/'spirit_legacy_worker.py'))) +OUT=Path(os.environ.get('TRELLIS_STUDIO_OUTPUTS','/home/foster/trellis2-outputs/studio')); OUT.mkdir(parents=True,exist_ok=True) +MAX_SEED=2147483647 +VIEWS=[('Front','front'),('Back','back'),('Left','left'),('Right','right'),('Top','top'),('Bottom','bottom'),('Extra View 1','extra_1'),('Extra View 2','extra_2')] +PRESETS={ + 'Phase 8 Safe':('512','multidiffusion',12,12,12,150000,'1024',8192), + 'Fast Preview':('512','stochastic',8,8,8,100000,'1024',8192), + 'Game Asset':('512','multidiffusion',12,12,12,250000,'2048',8192), + 'High-Res Master (Experimental)':('1024','multidiffusion',16,16,16,750000,'4096',24576), + 'Ultra 1536 (Experimental)':('1536','multidiffusion',20,20,20,1000000,'4096',32768), + 'Custom':None} + +def clean(v): + v=re.sub(r'[^A-Za-z0-9._-]+','_', (v or 'asset').strip()).strip('._-'); return v[:80] or 'asset' +def preset(name): + p=PRESETS.get(name); editable=p is None + if editable: return tuple(gr.update(interactive=True) for _ in range(8)) + return tuple(gr.update(value=v,interactive=False) for v in p) +def state(path): + try:return json.loads(path.read_text(encoding='utf-8')) + except Exception:return {} +def verify(label,path): + im=Image.open(path) + if im.mode!='RGBA' or im.getchannel('A').getextrema()[0]==255: + raise gr.Error(f'{label}: use an RGBA PNG with meaningful transparency.') + +def run(asset,category,preset_name,resolution,fusion,randomize,seed,ss_steps,ss_g,ss_gr,ss_t,shape_steps,shape_g,shape_gr,shape_t,tex_steps,tex_g,tex_gr,tex_t,decimation,texture,tokens,*files): + chosen=[] + for (display,label),path in zip(VIEWS,files): + if path: verify(display,path); chosen.append((display,label,Path(path))) + if not chosen: raise gr.Error('Select at least one reference image.') + seed=random.randint(0,MAX_SEED) if randomize else int(seed) + stamp=datetime.now().strftime('%Y%m%d_%H%M%S'); job_id=f'{clean(asset)}_{stamp}_{seed}'; jobdir=OUT/clean(category or 'Other')/job_id; inp=jobdir/'inputs'; inp.mkdir(parents=True) + viewargs=[]; inputs=[] + for _,label,src in chosen: + dst=inp/f'{label}{src.suffix.lower() or ".png"}'; shutil.copy2(src,dst); viewargs += ['--view',f'{label}={dst}']; inputs.append({'label':label,'original_filename':src.name,'stored_filename':dst.name}) + (jobdir/'request.json').write_text(json.dumps({'asset_name':asset,'category':category,'preset':preset_name,'resolution':int(resolution),'fusion_mode':fusion,'seed':seed,'inputs':inputs,'created_at':time.time()},indent=2),encoding='utf-8') + jf=jobdir/'job.json'; logp=jobdir/'worker.log' + cmd=[str(PYTHON),str(WORKER),'--job-file',str(jf),'--job-id',job_id,*viewargs,'--output-dir',str(jobdir),'--resolution',str(resolution),'--seed',str(seed),'--fusion-mode',fusion,'--ss-steps',str(int(ss_steps)),'--ss-guidance',str(ss_g),'--ss-guidance-rescale',str(ss_gr),'--ss-rescale-t',str(ss_t),'--shape-steps',str(int(shape_steps)),'--shape-guidance',str(shape_g),'--shape-guidance-rescale',str(shape_gr),'--shape-rescale-t',str(shape_t),'--tex-steps',str(int(tex_steps)),'--tex-guidance',str(tex_g),'--tex-guidance-rescale',str(tex_gr),'--tex-rescale-t',str(tex_t),'--decimation-target',str(int(decimation)),'--texture-size',str(texture),'--max-num-tokens',str(int(tokens)),'--low-vram'] + with logp.open('w',encoding='utf-8',buffering=1) as log: + proc=subprocess.Popen(cmd,stdout=log,stderr=subprocess.STDOUT,text=True) + while proc.poll() is None: + s=state(jf); yield f"Job: {job_id}\nViews: {len(chosen)} ({', '.join(x[1] for x in chosen)})\nSeed: {seed}\nResolution: {resolution}\nFusion: {fusion}\nPhase: {s.get('phase','starting')}",None,None; time.sleep(1) + s=state(jf) + if proc.returncode or s.get('status')!='completed': + try: tail='\n'.join(logp.read_text(encoding='utf-8').splitlines()[-25:]) + except Exception: tail='' + yield f"FAILED: {s.get('error',f'worker exit {proc.returncode}')}\n\n{tail}",None,str(jf); return + gp=Path(s['glb']); mp=Path(s.get('metadata',jobdir/'metadata.json')); yield f"Completed: {job_id}\nViews: {len(chosen)}\nSeed: {seed}\nResolution: {resolution}\nFusion: {fusion}\nTotal seconds: {s.get('total_seconds')}\nOutput: {gp}",str(gp),str(mp) + +with gr.Blocks(title='Spirit Legacy TRELLIS Studio') as demo: + gr.Markdown('# Spirit Legacy TRELLIS Studio\nSelect any views you have. Filenames do not matter. Transparent RGBA PNGs are required on this ROCm setup.') + with gr.Row(): + asset=gr.Textbox(label='Asset Name',placeholder='Greyhaven_Blacksmith_01',scale=2); category=gr.Dropdown(['Building','Tree','Prop','Weapon','Armor','Character','Creature','Environment','Other'],value='Prop',label='Category',allow_custom_value=True); preset_name=gr.Dropdown(list(PRESETS),value='Phase 8 Safe',label='Quality Preset') + gr.Markdown('### Reference views') + files=[] + for i in range(0,8,4): + with gr.Row(): + for display,_ in VIEWS[i:i+4]: files.append(gr.File(label=display,file_types=['.png'],type='filepath')) + with gr.Row(): + resolution=gr.Radio(['512','1024','1536'],value='512',label='Generation Resolution',interactive=False); fusion=gr.Radio(['multidiffusion','stochastic'],value='multidiffusion',label='Multi-View Fusion',interactive=False); randomize=gr.Checkbox(True,label='Randomize Seed'); seed=gr.Slider(0,MAX_SEED,42,step=1,label='Seed') + with gr.Accordion('Advanced Settings',open=False): + gr.Markdown('Choose **Custom** to edit preset-managed values. Guidance controls remain adjustable for fine tuning.') + with gr.Row(): ss_steps=gr.Slider(4,40,12,step=1,label='Sparse Steps',interactive=False); ss_g=gr.Slider(0,20,7.5,step=.1,label='Sparse Guidance'); ss_gr=gr.Slider(0,1,.7,step=.05,label='Sparse Guidance Rescale'); ss_t=gr.Slider(0,10,5,step=.1,label='Sparse Rescale T') + with gr.Row(): shape_steps=gr.Slider(4,40,12,step=1,label='Shape Steps',interactive=False); shape_g=gr.Slider(0,20,7.5,step=.1,label='Shape Guidance'); shape_gr=gr.Slider(0,1,.5,step=.05,label='Shape Guidance Rescale'); shape_t=gr.Slider(0,10,3,step=.1,label='Shape Rescale T') + with gr.Row(): tex_steps=gr.Slider(4,40,12,step=1,label='Texture Steps',interactive=False); tex_g=gr.Slider(0,10,1,step=.1,label='Texture Guidance'); tex_gr=gr.Slider(0,1,0,step=.05,label='Texture Guidance Rescale'); tex_t=gr.Slider(0,10,3,step=.1,label='Texture Rescale T') + with gr.Row(): decimation=gr.Slider(100000,1500000,150000,step=10000,label='GLB Decimation',interactive=False); texture=gr.Radio(['512','1024','2048','4096'],value='1024',label='Texture Size',interactive=False); tokens=gr.Slider(4096,49152,8192,step=1024,label='Max Tokens',interactive=False) + go=gr.Button('Generate 3D Asset',variant='primary'); status=gr.Textbox(label='Job Status',lines=8,interactive=False) + with gr.Row(): glb=gr.File(label='Generated GLB'); meta=gr.File(label='Generation Metadata') + preset_name.change(preset,[preset_name],[resolution,fusion,ss_steps,shape_steps,tex_steps,decimation,texture,tokens]) + go.click(run,[asset,category,preset_name,resolution,fusion,randomize,seed,ss_steps,ss_g,ss_gr,ss_t,shape_steps,shape_g,shape_gr,shape_t,tex_steps,tex_g,tex_gr,tex_t,decimation,texture,tokens,*files],[status,glb,meta]) +if __name__=='__main__': demo.queue(default_concurrency_limit=1).launch(server_name='0.0.0.0',server_port=int(os.environ.get('TRELLIS_STUDIO_PORT','7860')),show_error=True,allowed_paths=[str(OUT)]) diff --git a/tools/spirit_legacy_worker.py b/tools/spirit_legacy_worker.py new file mode 100755 index 00000000..8c430ee9 --- /dev/null +++ b/tools/spirit_legacy_worker.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Spirit Legacy TRELLIS.2 production worker.""" +from __future__ import annotations +import argparse,gc,json,os,sys,time,traceback +from contextlib import contextmanager +from pathlib import Path + +ROOT=Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: sys.path.insert(0,str(ROOT)) +os.environ["HSA_ENABLE_DXG_DETECTION"]="1" +os.environ["ATTN_BACKEND"]="sdpa" +os.environ["SPARSE_ATTN_BACKEND"]="sdpa" +os.environ["SPARSE_CONV_BACKEND"]="flex_gemm" +os.environ["PYTORCH_ROCM_ARCH"]="gfx1201" +os.environ["GPU_ARCHS"]="gfx1201" +os.environ["ROCM_HOME"]="/opt/rocm/core-7.14" +os.environ["ROCM_PATH"]="/opt/rocm/core-7.14" +os.environ["HIP_PATH"]="/opt/rocm/core-7.14" +os.environ["TORCH_EXTENSIONS_DIR"]="/home/foster/.cache/torch_extensions_trellis2_gfx1201" +os.environ["PYTHONPATH"]=f"{ROOT}:/home/foster/.cache/torch_extensions_trellis2_gfx1201/uv_rasterize_kernel" +os.environ["HF_HUB_OFFLINE"]="1" +os.environ["TRANSFORMERS_OFFLINE"]="1" +os.environ.pop("PYTORCH_CUDA_ALLOC_CONF",None) + +def atomic_json(path,value): + path.parent.mkdir(parents=True,exist_ok=True); tmp=path.with_suffix(path.suffix+".tmp") + tmp.write_text(json.dumps(value,indent=2,sort_keys=True),encoding="utf-8"); os.replace(tmp,path) + +def update(path,**changes): + try: state=json.loads(path.read_text(encoding="utf-8")) + except Exception: state={} + state.update(changes); state["updated_at"]=time.time(); atomic_json(path,state); return state + +def parse_views(values): + out=[]; seen=set() + for raw in values: + if "=" not in raw: raise ValueError(f"Invalid --view {raw!r}; expected LABEL=/path/file.png") + label,p=raw.split("=",1); label=label.strip().lower().replace(" ","_"); p=Path(p).expanduser().resolve() + if not label or label in seen: raise ValueError(f"Invalid or duplicate view label: {label}") + if not p.is_file(): raise FileNotFoundError(f"Missing {label} view: {p}") + seen.add(label); out.append((label,p)) + if not out: raise ValueError("At least one input image is required") + return out + +@contextmanager +def multi_sampler(sampler,n,steps,mode): + if n<=1: yield; return + old=sampler._inference_model; sampler._old_inference_model=old + try: + if mode=="stochastic": + if n>steps: print(f"Warning: {n} images exceeds {steps} sampler steps",flush=True) + counter={"v":0} + def wrapped(self,model,x_t,t,cond,**kwargs): + i=counter["v"]%n; counter["v"]+=1 + return self._old_inference_model(model,x_t,t,cond=cond[i:i+1],**kwargs) + elif mode=="multidiffusion": + from trellis2.pipelines.samplers import FlowEulerSampler + def wrapped(self,model,x_t,t,cond,neg_cond,guidance_strength,guidance_interval,guidance_rescale=0.0,**kwargs): + preds=[FlowEulerSampler._inference_model(self,model,x_t,t,cond[i:i+1],**kwargs) for i in range(len(cond))] + pred=sum(preds)/len(preds) + if guidance_interval[0] <= t <= guidance_interval[1]: + neg=FlowEulerSampler._inference_model(self,model,x_t,t,neg_cond,**kwargs) + cfg=guidance_strength*pred+(1-guidance_strength)*neg + if guidance_rescale>0: + xp=self._pred_to_xstart(x_t,t,pred); xc=self._pred_to_xstart(x_t,t,cfg); dims=list(range(1,xp.ndim)) + sp=xp.std(dim=dims,keepdim=True); sc=xc.std(dim=dims,keepdim=True).clamp_min(1e-6) + xr=xc*(sp/sc); cfg=self._xstart_to_pred(x_t,t,guidance_rescale*xr+(1-guidance_rescale)*xc) + return cfg + return pred + else: raise ValueError(f"Unsupported fusion mode: {mode}") + sampler._inference_model=wrapped.__get__(sampler,type(sampler)); yield + finally: + sampler._inference_model=old + if hasattr(sampler,"_old_inference_model"): delattr(sampler,"_old_inference_model") + +def cond(pipeline,images,res): + import torch + c=torch.cat([pipeline.get_cond([im],res)["cond"] for im in images],dim=0) + return {"cond":c,"neg_cond":torch.zeros_like(c[:1])} + +def generate(pipeline,images,seed,resolution,fusion,ss,shape,tex,max_tokens): + import torch + ptype={512:"512",1024:"1024_cascade",1536:"1536_cascade"}[resolution]; torch.manual_seed(seed) + c512=cond(pipeline,images,512); c1024=cond(pipeline,images,1024) if resolution!=512 else None; n=len(images) + with multi_sampler(pipeline.sparse_structure_sampler,n,ss["steps"],fusion): + coords=pipeline.sample_sparse_structure(c512,32,1,ss) + if resolution==512: + with multi_sampler(pipeline.shape_slat_sampler,n,shape["steps"],fusion): + slat=pipeline.sample_shape_slat(c512,pipeline.models["shape_slat_flow_model_512"],coords,shape) + tc=c512; tm=pipeline.models["tex_slat_flow_model_512"]; actual=512 + else: + with multi_sampler(pipeline.shape_slat_sampler,n,shape["steps"],fusion): + slat,actual=pipeline.sample_shape_slat_cascade(c512,c1024,pipeline.models["shape_slat_flow_model_512"],pipeline.models["shape_slat_flow_model_1024"],512,resolution,coords,shape,max_num_tokens=max_tokens) + tc=c1024; tm=pipeline.models["tex_slat_flow_model_1024"] + with multi_sampler(pipeline.tex_slat_sampler,n,tex["steps"],fusion): + tslat=pipeline.sample_tex_slat(tc,tm,slat,tex) + torch.cuda.empty_cache(); return pipeline.decode_latent(slat,tslat,actual) + +def main(): + ap=argparse.ArgumentParser(); ap.add_argument("--job-file",required=True); ap.add_argument("--job-id",required=True); ap.add_argument("--view",action="append",default=[]); ap.add_argument("--output-dir",required=True) + ap.add_argument("--resolution",type=int,choices=[512,1024,1536],default=512); ap.add_argument("--seed",type=int,default=42); ap.add_argument("--fusion-mode",choices=["stochastic","multidiffusion"],default="multidiffusion") + ap.add_argument("--ss-steps",type=int,default=12); ap.add_argument("--ss-guidance",type=float,default=7.5); ap.add_argument("--ss-guidance-rescale",type=float,default=.7); ap.add_argument("--ss-rescale-t",type=float,default=5.0) + ap.add_argument("--shape-steps",type=int,default=12); ap.add_argument("--shape-guidance",type=float,default=7.5); ap.add_argument("--shape-guidance-rescale",type=float,default=.5); ap.add_argument("--shape-rescale-t",type=float,default=3.0) + ap.add_argument("--tex-steps",type=int,default=12); ap.add_argument("--tex-guidance",type=float,default=1.0); ap.add_argument("--tex-guidance-rescale",type=float,default=0.0); ap.add_argument("--tex-rescale-t",type=float,default=3.0) + ap.add_argument("--decimation-target",type=int,default=150000); ap.add_argument("--texture-size",type=int,choices=[512,1024,2048,4096],default=1024); ap.add_argument("--max-num-tokens",type=int,default=8192); ap.add_argument("--low-vram",action=argparse.BooleanOptionalAction,default=True); a=ap.parse_args() + jf=Path(a.job_file).resolve(); od=Path(a.output_dir).resolve(); od.mkdir(parents=True,exist_ok=True); started=time.monotonic(); update(jf,status="running",phase="validating_input",pid=os.getpid(),started_at=time.time()) + try: + from PIL import Image + import torch,o_voxel + from trellis2.pipelines import Trellis2ImageTo3DPipeline + views=parse_views(a.view); images=[]; inputs=[] + for label,path in views: + im=Image.open(path) + if im.mode!="RGBA" or im.getchannel("A").getextrema()[0]==255: raise RuntimeError(f"{label} must be an RGBA PNG with meaningful transparency") + images.append(im); inputs.append({"label":label,"filename":path.name,"size":[im.width,im.height]}) + names=["sparse_structure_decoder","sparse_structure_flow_model","shape_slat_decoder","shape_slat_flow_model_512","tex_slat_decoder","tex_slat_flow_model_512"] + if a.resolution>512: names += ["shape_slat_flow_model_1024","tex_slat_flow_model_1024"] + Trellis2ImageTo3DPipeline.model_names_to_load=names; update(jf,phase="loading_pipeline",view_count=len(images),resolution=a.resolution); t=time.monotonic() + pipeline=Trellis2ImageTo3DPipeline.from_pretrained("microsoft/TRELLIS.2-4B",config_file="pipeline.json"); pipeline.low_vram=a.low_vram; pipeline.cuda(); load=time.monotonic()-t + ss={"steps":a.ss_steps,"guidance_strength":a.ss_guidance,"guidance_rescale":a.ss_guidance_rescale,"guidance_interval":[.6,1.0],"rescale_t":a.ss_rescale_t} + shape={"steps":a.shape_steps,"guidance_strength":a.shape_guidance,"guidance_rescale":a.shape_guidance_rescale,"guidance_interval":[.6,1.0],"rescale_t":a.shape_rescale_t} + tex={"steps":a.tex_steps,"guidance_strength":a.tex_guidance,"guidance_rescale":a.tex_guidance_rescale,"guidance_interval":[.6,.9],"rescale_t":a.tex_rescale_t} + update(jf,phase="generating",load_seconds=round(load,3)); t=time.monotonic(); meshes=generate(pipeline,images,a.seed,a.resolution,a.fusion_mode,ss,shape,tex,a.max_num_tokens); mesh=meshes[0]; gen=time.monotonic()-t + update(jf,phase="releasing_models"); pipeline.release_inference_models(); del meshes,pipeline; gc.collect(); torch.cuda.empty_cache() + update(jf,phase="exporting_glb",generation_seconds=round(gen,3)); t=time.monotonic() + glb=o_voxel.postprocess.to_glb(vertices=mesh.vertices,faces=mesh.faces,attr_volume=mesh.attrs,coords=mesh.coords,attr_layout=mesh.layout,voxel_size=mesh.voxel_size,aabb=[[-.5,-.5,-.5],[.5,.5,.5]],decimation_target=a.decimation_target,texture_size=a.texture_size,remesh=True,remesh_band=1,remesh_project=0,verbose=True) + gp=od/"model.glb"; glb.export(str(gp),extension_webp=True); export=time.monotonic()-t + meta={"job_id":a.job_id,"inputs":inputs,"resolution":a.resolution,"seed":a.seed,"fusion_mode":a.fusion_mode,"view_count":len(images),"samplers":{"sparse":ss,"shape":shape,"texture":tex},"max_num_tokens":a.max_num_tokens,"low_vram":a.low_vram,"decimation_target":a.decimation_target,"texture_size":a.texture_size,"glb":gp.name,"glb_bytes":gp.stat().st_size,"load_seconds":round(load,3),"generation_seconds":round(gen,3),"export_seconds":round(export,3),"total_seconds":round(time.monotonic()-started,3),"completed_at":time.time()} + mp=od/"metadata.json"; atomic_json(mp,meta); update(jf,status="completed",phase="completed",completed_at=time.time(),glb=str(gp),metadata=str(mp),glb_bytes=meta["glb_bytes"],total_seconds=meta["total_seconds"],exit_code=0); print(f"GLB_PATH={gp}",flush=True); return 0 + except BaseException as e: + update(jf,status="failed",phase="failed",completed_at=time.time(),error=f"{type(e).__name__}: {e}"[:1000],traceback=traceback.format_exc()[-12000:],exit_code=1); traceback.print_exc(); return 1 +if __name__=="__main__": raise SystemExit(main()) diff --git a/tools/start_spirit_legacy_gui.sh b/tools/start_spirit_legacy_gui.sh new file mode 100755 index 00000000..4b4c8f8c --- /dev/null +++ b/tools/start_spirit_legacy_gui.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PYTHON="${TRELLIS_PYTHON:-/home/foster/trellis2-env/bin/python}" +PORT="${TRELLIS_STUDIO_PORT:-7860}" +cd "$REPO" +export TRELLIS_STUDIO_PORT="$PORT" +export PYTHONUNBUFFERED=1 +exec "$PYTHON" "$REPO/tools/spirit_legacy_gui.py"