diff --git a/docs/developer_guides/viewer/imgs/viewer_dark.png b/docs/developer_guides/viewer/imgs/viewer_dark.png deleted file mode 100644 index d2abe9a10..000000000 Binary files a/docs/developer_guides/viewer/imgs/viewer_dark.png and /dev/null differ diff --git a/docs/developer_guides/viewer/imgs/viewer_light.png b/docs/developer_guides/viewer/imgs/viewer_light.png deleted file mode 100644 index 2220e3a8c..000000000 Binary files a/docs/developer_guides/viewer/imgs/viewer_light.png and /dev/null differ diff --git a/docs/developer_guides/viewer/viewer_overview.md b/docs/developer_guides/viewer/viewer_overview.md deleted file mode 100644 index 55dbab2f2..000000000 --- a/docs/developer_guides/viewer/viewer_overview.md +++ /dev/null @@ -1,98 +0,0 @@ -# Viewer overview - -> We have a real-time web viewer that requires no installation. It's available at [https://viewer.nerf.studio/](https://viewer.nerf.studio/), where you can connect to your training job. If you want to understand how the viewer works and contribute to it's features, this section is for you! - -The viewer is built using [ThreeJS](https://threejs.org/) and packaged into a [ReactJS](https://reactjs.org/) application. This client viewer application will connect via a websocket to a server running on your machine. The following figure helps to illustrate how our viewer framework works: - -```{image} imgs/viewer_light.png -:align: center -:class: only-light -``` - -```{image} imgs/viewer_dark.png -:align: center -:class: only-dark -``` - -## System design - -#### Connection between nerfstudio and the Bridge Server - -In the center, we have the Bridge Server, which facilitates the connection between nerfstudio code and the Client App. This server runs on the same machine that you are using nerfstudio. It has a TCP Request/Reply (REQ/REP) connection that nerfstudio can connect to with the Viewer object (left). We use [ZeroMQ](https://zeromq.org/), an open-sourced messaging library, to implement this lightweight TCP connection. The Viewer class can send commands to the Bridge Server and receive replies. The Bridge Server will either dispatch commands to the Client App via a websocket or it will return information stored in the Bridge Server state. - -#### Connection between the Bridge Server and the Client App - -The connection between the Bridge Server and the Client App works with WebSockets. - -- **WebSocket connection** - The WebSocket is used by the Bridge Server to dispatch commands coming from the nerfstudio TCP connection. Commands can be used for drawing primitives, for setting the transform of objects, for the setting various properties, and more. - -## Installing and running locally - -#### Running the Client App - -```shell -cd nerfstudio/viewer/app -``` - -Install npm (to install yarn) and yarn - -```shell -sudo apt-get install npm -npm install --global yarn -``` - -Install nvm and set the node version -Install nvm with [instructions](https://heynode.com/tutorial/install-nodejs-locally-nvm/). - -```shell -nvm install 17.8.0 -``` - -Now running `node --version` in the shell should print "v17.8.0". -Install package.json dependencies and start the client viewer app - -```shell -yarn install -yarn start -``` - -The local webserver runs on port 4000 by default, -so when `ns-train` is running, you can connect to the viewer locally at -[http://localhost:4000/?websocket_url=ws://localhost:7007](http://localhost:4000/?websocket_url=ws://localhost:7007) - -#### Running the Bridge Server Manually - -The viewer bridge server runs on the same machine that you use for training. The training code will connect to this server with a lightweight TCP connection using ZMQ. The training job will launch the viewer bridge server is you specify `--viewer.launch-bridge-server` in the terminal. Otherwise, you can launch the bridge server manually with the following script. - -```python -ns-bridge-server --help -``` - -## Acknowledgements and references - -We thank the authors and contributors to the following repos, which we've started, used, and modified for our use-cases. - -- [meshcat-python](https://github.com/rdeits/meshcat-python) - made by [Robin Deits](https://github.com/rdeits) -- [meshcat](https://github.com/rdeits/meshcat) - made by [Robin Deits](https://github.com/rdeits) -- [ThreeJS](https://threejs.org/) -- [ReactJS](https://reactjs.org/) - -## FAQ - -#### Engine node incompatible - -While running `yarn install`, you run into: `The engine "node" is incompatible with this module.` - -**Solution**: - -Install nvm with instructions at [instructions](https://heynode.com/tutorial/install-nodejs-locally-nvm/). - -```shell -nvm install 17.8.0 -``` - -If you cannot install nvm, try ignoring the engines - -``` -yarn install --ignore-engines -``` diff --git a/nerfstudio/configs/method_configs.py b/nerfstudio/configs/method_configs.py index 5b8415b74..80aa18908 100644 --- a/nerfstudio/configs/method_configs.py +++ b/nerfstudio/configs/method_configs.py @@ -457,13 +457,12 @@ method_configs["dreamfusion"] = TrainerConfig( pipeline=VanillaPipelineConfig( generative=True, datamanager=DreamFusionDataManagerConfig( - horizontal_rotation_warmup=0, + horizontal_rotation_warmup=2000, ), model=DreamFusionModelConfig( eval_num_rays_per_chunk=1 << 15, distortion_loss_mult=0.02, interlevel_loss_mult=100.0, - orientation_loss_mult=0.1, max_res=256, sphere_collider=True, initialize_density=True, diff --git a/nerfstudio/data/datamanagers/dreamfusion_datamanager.py b/nerfstudio/data/datamanagers/dreamfusion_datamanager.py index 6a47fc53b..6ce1ed2ff 100644 --- a/nerfstudio/data/datamanagers/dreamfusion_datamanager.py +++ b/nerfstudio/data/datamanagers/dreamfusion_datamanager.py @@ -168,7 +168,7 @@ class DreamFusionDataManagerConfig(DataManagerConfig): """Mean radius of camera orbit""" radius_std: float = 0.1 """Std of radius of camera orbit""" - focal_range: Tuple[float, float] = (0.6, 1.2) + focal_range: Tuple[float, float] = (0.7, 1.35) """Range of focal length""" vertical_rotation_range: Tuple[float, float] = (-90, 0) """Range of vertical rotation""" diff --git a/nerfstudio/fields/dreamfusion_field.py b/nerfstudio/fields/dreamfusion_field.py index 9c78513fc..db2d75ddc 100644 --- a/nerfstudio/fields/dreamfusion_field.py +++ b/nerfstudio/fields/dreamfusion_field.py @@ -113,20 +113,6 @@ class DreamFusionField(Field): }, ) - # predicted normals - self.mlp_pred_normals = tcnn.Network( - n_input_dims=self.geo_feat_dim + self.position_encoding.n_output_dims, - n_output_dims=hidden_dim_normal, - network_config={ - "otype": "FullyFusedMLP", - "activation": "ReLU", - "output_activation": "None", - "n_neurons": 64, - "n_hidden_layers": 2, - }, - ) - self.field_head_pred_normals = PredNormalsFieldHead(in_dim=self.mlp_pred_normals.n_output_dims) - self.mlp_background_color = tcnn.Network( n_input_dims=self.direction_encoding.n_output_dims, n_output_dims=3, @@ -186,15 +172,6 @@ class DreamFusionField(Field): outputs_shape = ray_samples.frustums.directions.shape[:-1] - # predicted normals - positions = ray_samples.frustums.get_positions() - - positions_flat = self.position_encoding(positions.view(-1, 3)) - pred_normals_inp = torch.cat([positions_flat, density_embedding.view(-1, self.geo_feat_dim)], dim=-1) - - x = self.mlp_pred_normals(pred_normals_inp).view(*outputs_shape, -1).to(directions) - outputs[FieldHeadNames.PRED_NORMALS] = self.field_head_pred_normals(x) - h = density_embedding.view(-1, self.geo_feat_dim) rgb = self.mlp_head(h).view(*outputs_shape, -1).to(directions) diff --git a/nerfstudio/generative/deepfloyd.py b/nerfstudio/generative/deepfloyd.py index 76ae54609..3b5f3a41b 100644 --- a/nerfstudio/generative/deepfloyd.py +++ b/nerfstudio/generative/deepfloyd.py @@ -117,7 +117,7 @@ class DeepFloyd(nn.Module): negative_prompt = [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt prompt_embeds, negative_embeds = self.pipe.encode_prompt(prompt, negative_prompt=negative_prompt) - return torch.cat([prompt_embeds, negative_embeds]) + return torch.cat([negative_embeds, prompt_embeds]) def sds_loss( self, @@ -140,7 +140,7 @@ class DeepFloyd(nn.Module): noise_pred = self.unet(image_model_input, t, encoder_hidden_states=text_embeddings).sample # perform guidance - noise_pred_text, noise_pred_uncond = noise_pred.chunk(2) + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) noise_pred_text, predicted_variance = noise_pred_text.split(3, dim=1) noise_pred_uncond, _ = noise_pred_uncond.split(3, dim=1) @@ -202,7 +202,6 @@ def generate_image( df = DeepFloyd(cuda_device) imgs = df.prompt_to_img(prompt, negative, generator, steps) imgs[0].save(save_path) - # mediapy.write_image(str(save_path), imgs[0]) if __name__ == "__main__": tyro.cli(generate_image) \ No newline at end of file diff --git a/nerfstudio/models/dreamfusion.py b/nerfstudio/models/dreamfusion.py index ecbc40882..052a3999e 100644 --- a/nerfstudio/models/dreamfusion.py +++ b/nerfstudio/models/dreamfusion.py @@ -67,14 +67,13 @@ class DreamFusionModelConfig(ModelConfig): _target: Type = field(default_factory=lambda: DreamFusionModel) """target class to instantiate""" - # prompt: str = "A high-quality photo of a pineapple" - # prompt: str = "A DSLR photo of an orangutan making a clay bowl on a throwing wheel" - # prompt: str = "A DSLR photo of a tiger dressed as a doctor" - prompt: str = "a high quality photo of an all utility vehicle driving across a stream" + prompt: str = "a high quality photo of a tree frog" """prompt for stable dreamfusion""" - orientation_loss_mult: float = 0.0001 + orientation_loss_mult: Tuple[float, float] = (0.01, 100.0) """Orientation loss multipier on computed normals.""" + orientation_loss_mult_end: int = 10000 + """number of iterations to reach last orientation_loss_mult value""" pred_normal_loss_mult: float = 0.001 """Predicted normal loss multiplier.""" random_light_source: bool = True @@ -155,8 +154,10 @@ class DreamFusionModelConfig(ModelConfig): """appended to prompt for back view""" guidance_scale: float = 20 """guidance scale for sds loss""" - stablediffusion_device: Optional[str] = None - """device for stable diffusion""" + diffusion_device: Optional[str] = None + """device for diffusion model""" + diffusion_model: Literal["stablediffusion", "deepfloyd"] = "deepfloyd" + """diffusion model for SDS loss""" sd_version: str = "1-5" @@ -192,10 +193,10 @@ class DreamFusionModel(Model): self.back_prompt = config.back_prompt self.front_prompt = config.front_prompt - self.sd_device = ( + self.diffusion_device = ( torch.device(kwargs["device"]) - if config.stablediffusion_device is None - else torch.device(config.stablediffusion_device) + if config.diffusion_device is None + else torch.device(config.diffusion_device) ) super().__init__(config=config, **kwargs) @@ -204,16 +205,18 @@ class DreamFusionModel(Model): """Set the fields and modules""" super().populate_modules() - # self.sd = StableDiffusion(self.sd_device, version=self.sd_version) - self._df = DeepFloyd(self.sd_device) + if self.config.diffusion_model == "stablediffusion": + self._diffusion_model = StableDiffusion(self.diffusion_device, version=self.sd_version) + elif self.config.diffusion_model == "deepfloyd": + self._diffusion_model = DeepFloyd(self.diffusion_device) + self.text_embeddings = PositionalTextEmbeddings( base_prompt=self.cur_prompt, top_prompt=self.cur_prompt + self.top_prompt, side_prompt=self.cur_prompt + self.side_prompt, back_prompt=self.cur_prompt + self.back_prompt, front_prompt=self.cur_prompt + self.front_prompt, - # stable_diffusion=self.sd, - diffusion_model=self._df, + diffusion_model=self._diffusion_model, positional_prompting=self.positional_prompting, ) @@ -289,17 +292,15 @@ class DreamFusionModel(Model): ): self.train_shaded = True - def update_target_transmittance( + def update_orientation_loss_mult( self, training_callback_attributes: TrainingCallbackAttributes, step: int # pylint: disable=unused-argument ): - if ( - step < self.config.transmittance_end_schedule - and self.target_transmittance > self.config.target_transmittance_end - ): - self.target_transmittance -= (1 / self.config.transmittance_end_schedule) * ( - self.config.target_transmittance_start - self.config.target_transmittance_end - ) - + if step <= self.config.start_normals_training: + self.orientation_loss_mult = 0 + else: + self.orientation_loss_mult = np.interp(step, self.config.orientation_loss_mult, + (self.config.start_normals_training, self.config.orientation_loss_mult_end)) + # anneal the weights of the proposal network before doing PDF sampling def set_anneal(step): # https://arxiv.org/pdf/2111.12077.pdf eq. 18 @@ -330,7 +331,7 @@ class DreamFusionModel(Model): TrainingCallback( where_to_run=[TrainingCallbackLocation.AFTER_TRAIN_ITERATION], update_every_num_iters=1, - func=update_target_transmittance, + func=update_orientation_loss_mult, args=[self, training_callback_attributes], ), TrainingCallback( @@ -361,8 +362,8 @@ class DreamFusionModel(Model): if self.initialize_density: pos = ray_samples.frustums.get_positions() - density_blob = self.density_strength * torch.exp(-torch.norm(pos, dim=-1) / (2 * 0.04))[..., None] - density = density + density_blob + density_blob = self.density_strength * (-torch.exp(torch.norm(pos, dim=-1) / 0.4) + 2)[..., None] + density = torch.max(density + density_blob, torch.tensor([0.0], device=self.device)) weights = ray_samples.get_weights(density) weights_list.append(weights) @@ -394,7 +395,6 @@ class DreamFusionModel(Model): outputs[f"prop_depth_{i}"] = self.renderer_depth(weights=weights_list[i], ray_samples=ray_samples_list[i]) normals = self.renderer_normals(normals=field_outputs[FieldHeadNames.NORMALS], weights=weights) - pred_normals = self.renderer_normals(field_outputs[FieldHeadNames.PRED_NORMALS], weights=weights) # lambertian shading if self.config.random_light_source: # and self.training: @@ -415,7 +415,6 @@ class DreamFusionModel(Model): shaded, shaded_albedo = accum_mask * shaded, accum_mask * shaded_albedo outputs["normals"] = self.shader_normals(normals, weights=accum_mask) - outputs["pred_normals"] = self.shader_normals(pred_normals, weights=accum_mask) outputs["shaded"] = shaded outputs["other_train_output"] = shaded_albedo + background outputs["shaded_albedo"] = shaded_albedo @@ -450,12 +449,6 @@ class DreamFusionModel(Model): weights.detach(), field_outputs[FieldHeadNames.NORMALS], ray_bundle.directions ) - outputs["rendered_pred_normal_loss"] = pred_normal_loss( - weights.detach(), - field_outputs[FieldHeadNames.NORMALS].detach(), - field_outputs[FieldHeadNames.PRED_NORMALS], - ) - assert weights.shape[-1] == 1 if self.config.opacity_penalty: outputs["opacity_loss"] = torch.sqrt(torch.sum(weights, dim=-2) ** 2 + 0.01) * self.config.opacity_loss_mult @@ -469,16 +462,11 @@ class DreamFusionModel(Model): loss_dict = misc.scale_dict(loss_dict, self.config.loss_coefficients) if self.train_normals: # orientation loss for computed normals - loss_dict["orientation_loss"] = self.config.orientation_loss_mult * torch.mean( + loss_dict["orientation_loss"] = self.orientation_loss_mult * torch.mean( outputs["rendered_orientation_loss"] ) - # ground truth supervision for normals - loss_dict["pred_normal_loss"] = self.config.pred_normal_loss_mult * torch.mean( - outputs["rendered_pred_normal_loss"] - ) else: loss_dict["orientation_loss"] = 0 - loss_dict["pred_normal_loss"] = 0 if self.config.opacity_penalty: loss_dict["opacity_loss"] = self.config.opacity_loss_mult * outputs["opacity_loss"].mean() @@ -503,16 +491,9 @@ class DreamFusionModel(Model): .permute(0, 3, 1, 2) ) - # sds_loss = self.sd.sds_loss( - # text_embedding.to(self.sd_device), - # train_output.to(self.sd_device), - # guidance_scale=int(self.guidance_scale), - # grad_scaler=self.grad_scaler, - # ) - - sds_loss = self._df.sds_loss( - text_embedding.to(self.sd_device), - train_output.to(self.sd_device), + sds_loss = self._diffusion_model.sds_loss( + text_embedding.to(self.diffusion_device), + train_output.to(self.diffusion_device), guidance_scale=int(self.guidance_scale), grad_scaler=self.grad_scaler, ) @@ -553,6 +534,5 @@ class DreamFusionModel(Model): "prop_depth_0": prop_depth_0, "prop_depth_1": prop_depth_1, "normals": outputs["normals"], - "pred_normals": outputs["pred_normals"], } return metrics_dict, images_dict diff --git a/nerfstudio/viewer/app/src/modules/RenderWindow/RenderWindow.jsx b/nerfstudio/viewer/app/src/modules/RenderWindow/RenderWindow.jsx deleted file mode 100644 index e2e1c53e7..000000000 --- a/nerfstudio/viewer/app/src/modules/RenderWindow/RenderWindow.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; -import { useSelector } from 'react-redux'; - -export default function RenderWindow() { - const render_img = useSelector((state) => state.render_img); - - return ( -
- Render window -
- ); -} diff --git a/nerfstudio/viewer/server/server.py b/nerfstudio/viewer/server/server.py deleted file mode 100644 index dd2b62235..000000000 --- a/nerfstudio/viewer/server/server.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright 2022 The Nerfstudio Team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Server bridge to facilitate interactions between python backend and javascript front end""" - -import sys -from typing import List, Optional, Tuple - -import tornado.gen -import tornado.ioloop -import tornado.web -import tornado.websocket -import tyro -import umsgpack -import zmq -import zmq.eventloop.ioloop -from pyngrok import ngrok -from zmq.eventloop.zmqstream import ZMQStream - -from nerfstudio.viewer.server.state.node import find_node, get_tree, walk -from nerfstudio.viewer.server.state.state_node import StateNode - - -class WebSocketHandler(tornado.websocket.WebSocketHandler): # pylint: disable=abstract-method - """Tornado websocket handler for receiving and sending commands from/to the viewer.""" - - def __init__(self, *args, **kwargs): - self.bridge = kwargs.pop("bridge") - super().__init__(*args, **kwargs) - - def check_origin(self, origin): - """This disables CORS.""" - return True - - def open(self, *args: str, **kwargs: str): - """open websocket bridge""" - self.bridge.websocket_pool.add(self) - print("opened:", self, file=sys.stderr) - self.bridge.send_scene(self) - - async def on_message(self, message: bytearray): # pylint: disable=invalid-overridden-method - """On reception of message from the websocket, - parses the message and calls the appropriate function based on the type of command - - Args: - message: byte message to parse - """ - data = message - m = umsgpack.unpackb(message) - type_ = m["type"] - path = list(filter(lambda x: len(x) > 0, m["path"].split("/"))) - - if type_ == "write": - # writes the data coming from the websocket - find_node(self.bridge.state_tree, path).data = m["data"] - command = {"type": "write", "path": m["path"], "data": m["data"]} - packed_data = umsgpack.packb(command) - frames = ["write".encode("utf-8"), m["path"].encode("utf-8"), packed_data] - self.bridge.forward_to_websockets(frames, websocket_to_skip=self) - elif type_ == "read": - # reads and returns the data - data = find_node(self.bridge.state_tree, path).data - self.write_message(data, binary=True) - else: - cmd_data = { - "type": "error", - "path": "", - "data": {"error": "Unknown command type: " + type_}, - } - data = umsgpack.packb(cmd_data) - self.write_message(data, binary=True) - - def on_close(self): - self.bridge.websocket_pool.remove(self) - print("closed:", self, file=sys.stderr) - - -class ZMQWebSocketBridge: - """ZMQ web socket bridge class - - Args: - zmq_port: zmq port to connect to. Defaults to None. - websocket_port: websocket port to connect to. Defaults to None. - """ - - context = zmq.Context() # pylint: disable=abstract-class-instantiated - - def __init__(self, zmq_port: int, websocket_port: int, ip_address: str): - self.zmq_port = zmq_port - self.websocket_pool = set() - self.app = self.make_app() - self.ioloop = tornado.ioloop.IOLoop.current() - - # zmq - zmq_url = f"tcp://{ip_address}:{self.zmq_port:d}" - self.zmq_socket, self.zmq_stream, self.zmq_url = self.setup_zmq(zmq_url) - - # websocket - listen_kwargs = {"address": "0.0.0.0"} - self.app.listen(websocket_port, **listen_kwargs) - self.websocket_port = websocket_port - self.websocket_url = f"0.0.0.0:{self.websocket_port}" - - # state tree - self.state_tree = get_tree(StateNode) - - def __str__(self) -> str: - class_name = self.__class__.__name__ - return f'{class_name} using zmq_port="{self.zmq_port}" and websocket_port="{self.websocket_port}"' - - def make_app(self): - """Create a tornado application for the websocket server.""" - return tornado.web.Application([(r"/", WebSocketHandler, {"bridge": self})]) - - def handle_zmq(self, frames: List[bytes]): - """Switch function that places commands in tree based on websocket command - - Args: - frames: the list containing command + object to be placed in tree - """ - if len(frames) != 3: - self.zmq_socket.send(b"error: expected 3 frames") - return - type_ = frames[0].decode("utf-8") - path = list(filter(lambda x: len(x) > 0, frames[1].decode("utf-8").split("/"))) - data = frames[2] - - if type_ == "write": - # TODO: use state_tree name - unpacked_data = umsgpack.unpackb(data) - find_node(self.state_tree, path).data = unpacked_data["data"] - self.forward_to_websockets(frames) - self.zmq_socket.send(umsgpack.packb(b"ok")) - elif type_ == "read": - # walk the node from the specified to get the full state dictionary - # TODO(ethan): handle the "data" key... - read_data = find_node(self.state_tree, path).data - self.zmq_socket.send(umsgpack.packb(read_data)) - else: - self.zmq_socket.send(umsgpack.packb(b"error: unknown command")) - - def forward_to_websockets( - self, frames: Tuple[str, str, bytes], websocket_to_skip: Optional[WebSocketHandler] = None - ): - """Forward a zmq message to all websockets. - - Args: - frames: byte messages to be sent over - """ - _, _, data = frames # cmd, path, data - for websocket in self.websocket_pool: - if websocket_to_skip and websocket == websocket_to_skip: - pass - else: - websocket.write_message(data, binary=True) - - def setup_zmq(self, url: str): - """Setup a zmq socket and connect it to the given url. - - Args: - url: point of connection - """ - zmq_socket = self.context.socket(zmq.REP) # pylint: disable=no-member - zmq_socket.bind(url) - zmq_stream = ZMQStream(zmq_socket) - zmq_stream.on_recv(self.handle_zmq) - return zmq_socket, zmq_stream, url - - def send_scene(self, websocket: WebSocketHandler): - """Sends entire tree of information over the specified websocket - - Args: - websocket: websocket to send information over - """ - print("Sending entire scene state due to websocket connection established.") - for path, node in walk("", self.state_tree): - if node.data is not None: - command = {"type": "write", "path": path, "data": node.data} - websocket.write_message(umsgpack.packb(command), binary=True) - - def run(self): - """starts and runs the websocket bridge""" - self.ioloop.start() - - -def run_viewer_bridge_server( - zmq_port: int = 6000, websocket_port: int = 7007, ip_address: str = "127.0.0.1", use_ngrok: bool = False -): - """Run the viewer bridge server. - - Args: - zmq_port: port to use for zmq - websocket_port: port to use for websocket - ip_address: host to connect to - use_ngrok: whether to use ngrok to expose the zmq port - """ - - # whether to launch pyngrok or not - if use_ngrok: - # Open a HTTP tunnel on the default port 80 - # .ngrok.io" -> "http://localhost:80"> - http_tunnel = ngrok.connect(addr=str(zmq_port), proto="tcp") - print(http_tunnel) - - bridge = ZMQWebSocketBridge(zmq_port=zmq_port, websocket_port=websocket_port, ip_address=ip_address) - print(bridge) - try: - bridge.run() - except KeyboardInterrupt: - pass - - -def entrypoint(): - """The main entrypoint.""" - tyro.extras.set_accent_color("bright_yellow") - tyro.cli(run_viewer_bridge_server) - - -if __name__ == "__main__": - entrypoint()