radames / Gradio-Blender-bpy

Running Gradio with Blender BPY engine
https://huggingface.co/spaces/radames/gradio-blender-bpy
1 stars 0 forks source link

Making molecularnodes a stand-alone web app with gradio 🧬 #1

Open kolibril13 opened 1 year ago

kolibril13 commented 1 year ago

hi @radames thanks for creating this project, which converts python blender pipelines to stand-alone web apps.

I want to share an idea for a follow-up project: @bradyajohnston just created the "molecularnode" package, which brings rendering of molecules to python. here's his recent tweet about that:

image

Now, I think it would be awesome to convert these molecule plotting pipelines into a web app as well.

Therefore, I've made a fork of Gradio-Blender-bpy and simplified the code into a minimal example, just the basic square. https://huggingface.co/spaces/kolibril13/gradio-molecule-blender-bpy

Then I created two notebooks for debugging, one that is very close to the minimal webapp, another one with 6 extra lines that also import the molecule. That works all correcly.

image

However, when I now modify the webapp by adding the line mn.load.molecule_rcsb to the minimal webapp and uncomment these 6 lines with the "#":

import gradio as gr
import bpy
from tqdm import tqdm
from math import pi
import tempfile
import molecularnodes as mn

def generate(progress=gr.Progress(track_tqdm=True)):

    # for obj in bpy.context.scene.objects:
    #   if obj.type == 'MESH':
    #       bpy.data.objects.remove(obj, do_unlink=True)

    # import molecularnodes as mn
    # molecule = mn.load.molecule_rcsb("7TYG", starting_style="cartoon", center_molecule=True) # <- this line would causes the error 🐛
    # molecule.select_set(True)

    bpy.ops.view3d.camera_to_view_selected()
    camera = bpy.data.objects["Camera"]
    camera.data.dof.use_dof = True
    camera.data.dof.focus_distance = 5
    camera.data.dof.aperture_fstop = 4
    camera.data.angle = pi / 3
    camera.data.type = "PERSP"

    with tempfile.NamedTemporaryFile(suffix=".JPEG", delete=False) as f:
        bpy.context.scene.render.resolution_y = 288
        bpy.context.scene.render.resolution_x = 512
        bpy.context.scene.render.image_settings.file_format = "JPEG"
        bpy.context.scene.render.filepath = f.name

        with tqdm() as pbar:

            def elapsed(dummy):
                pbar.update()

            bpy.app.handlers.render_stats.append(elapsed)
            bpy.context.scene.frame_set(1)
            bpy.context.scene.frame_current = 1
            bpy.ops.render.render(animation=False, write_still=True)

            bpy.data.images["Render Result"].save_render(
                filepath=bpy.context.scene.render.filepath
            )
            bpy.app.handlers.render_stats.clear()
            return f.name

with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column():
            render_btn = gr.Button("Render")
        with gr.Column(scale=3):
            image = gr.Image(type="filepath")

    render_btn.click(
        generate,
        outputs=[image],
    )

demo.queue(concurrency_count=1)
demo.launch(debug=True, inline=True)

I get a "context is incorrect" the error:

Traceback (most recent call last):
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/gradio/queueing.py", line 406, in call_prediction
    output = await route_utils.call_process_api(
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/gradio/route_utils.py", line 217, in call_process_api
    output = await app.get_blocks().process_api(
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/gradio/blocks.py", line 1553, in process_api
    result = await self.call_function(
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/gradio/blocks.py", line 1191, in call_function
    prediction = await anyio.to_thread.run_sync(
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/anyio/to_thread.py", line 33, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 877, in run_sync_in_worker_thread
    return await future
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 807, in run
    result = context.run(func, *args)
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/gradio/utils.py", line 659, in wrapper
    response = f(*args, **kwargs)
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/gradio/utils.py", line 659, in wrapper
    response = f(*args, **kwargs)
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/app.py", line 16, in generate
    molecule = mn.load.molecule_rcsb("7TYG", starting_style="cartoon", center_molecule=True) # <- this line causes the error 🐛
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/molecularnodes/load.py", line 116, in molecule_rcsb
    nodes.create_starting_node_tree(
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/molecularnodes/nodes.py", line 349, in create_starting_node_tree
    node_color_set = add_custom_node_group(node_mod, 'MN_color_set', [200, 0])
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/molecularnodes/nodes.py", line 122, in add_custom_node_group
    append(node_name, link=link)
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/molecularnodes/nodes.py", line 43, in append
    bpy.ops.wm.append(
  File "/Users/jan-hendrik/projects/gradio-molecule-blender-bpy/.venv/lib/python3.10/site-packages/bpy/3.6/scripts/modules/bpy/ops.py", line 113, in __call__
    ret = _op_call(self.idname_py(), None, kw)
RuntimeError: Operator bpy.ops.wm.append.poll() failed, context is incorrect

this seems to be a common error (see https://blender.stackexchange.com/questions/6101/poll-failed-context-incorrect-example-bpy-ops-view3d-background-image-add).

Do you have an idea how to set the default context here the right way?

BradyAJohnston commented 1 year ago

The context error is happening with this operator call:

https://github.com/BradyAJohnston/MolecularNodes/blob/0fbddfc14a4f8770a26d7ca1227178c21baebfa0/MolecularNodes/nodes.py#L62-L71

It hasn't proved to be problematic in the past running scripts via bpy. You could maybe try overriding the context, just for that function call: https://docs.blender.org/api/current/bpy.ops.html#overriding-context

But it might be something that has to be handled more internally with MN.

radames commented 1 year ago

I'v got something to render, but still didn't see the molecule. Using one tricks from the stackexchange, also didn't work on the first click, but render something on the second click. I guess something with starting the context. I don't know much about bpy, but it looks like the context override might be a good direction. Since in the context of Gradio, every new call to generate, should start from a new fresh context. do you have more directions here @BradyAJohnston? thanks

import gradio as gr
import bpy
from tqdm import tqdm
from math import pi
import tempfile
import molecularnodes as mn

window = bpy.context.window
screen = window.screen

def get_areas(type):
    return [area for area in screen.areas if area.type == type]

def get_regions(areas):
    return [region for region in areas[0].regions if region.type == 'WINDOW']

def generate(progress=gr.Progress(track_tqdm=True)):
    area_type = 'VIEW_3D' 
    areas  = get_areas(area_type)
    with bpy.context.temp_override(window=window, area=areas[0], region=get_regions(areas)[0], screen=screen):

        for obj in bpy.context.scene.objects:
            if obj.type == 'MESH':
                bpy.data.objects.remove(obj, do_unlink=True)

        molecule = mn.load.molecule_rcsb("7TYG", starting_style="cartoon", center_molecule=True) # <- this line would causes the error 🐛
        molecule.select_set(True)
        bpy.context.view_layer.objects.active = molecule

        bpy.ops.view3d.camera_to_view_selected()
        camera = bpy.data.objects["Camera"]
        camera.data.dof.use_dof = True
        camera.data.dof.focus_distance = 5
        camera.data.dof.aperture_fstop = 4
        camera.data.angle = pi / 3
        camera.data.type = "PERSP"

        with tempfile.NamedTemporaryFile(suffix=".JPEG", delete=False) as f:
            bpy.context.scene.render.resolution_y = 1000
            bpy.context.scene.render.resolution_x = 1000
            bpy.context.scene.render.image_settings.file_format = "JPEG"
            bpy.context.scene.render.filepath = f.name

            with tqdm() as pbar:

                def elapsed(dummy):
                    pbar.update()

                bpy.app.handlers.render_stats.append(elapsed)
                bpy.context.scene.frame_set(1)
                bpy.context.scene.frame_current = 1
                bpy.ops.render.render(animation=False, write_still=True)

                bpy.data.images["Render Result"].save_render(
                    filepath=bpy.context.scene.render.filepath
                )
                bpy.app.handlers.render_stats.clear()
                return f.name

with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column():
            render_btn = gr.Button("Render")
        with gr.Column(scale=3):
            image = gr.Image(type="filepath")

    render_btn.click(
        generate,
        outputs=[image],
    )

demo.queue(concurrency_count=1)
demo.launch(debug=True, inline=True)
BradyAJohnston commented 1 year ago

I've got it working initially. Definitely a context problem, and should be one that I can fix in molecularnodes itself by specifying the context.

I got around it by appending the required nodes first:

https://huggingface.co/spaces/bradyajohnston/gradio-molecular-nodes/blob/main/app.py

CleanShot 2023-10-05 at 11 16 41@2x

import gradio as gr
import bpy
from tqdm import tqdm
from math import pi
import tempfile
import molecularnodes as mn
import os

window = bpy.context.window
screen = window.screen

style = 'cartoon'

nodes_to_append = ["MN_color_set", 
                   "MN_color_common", 
                   "MN_color_attribute_random",
                   mn.nodes.styles_mapping[style]]

def get_areas(type):
    return [area for area in screen.areas if area.type == type]

def get_regions(areas):
    return [region for region in areas[0].regions if region.type == 'WINDOW']

for node in nodes_to_append:
    bpy.ops.wm.append(
                'INVOKE_DEFAULT',
                directory = os.path.join(mn.nodes.mn_data_file, 'NodeTree'), 
                filename = node, 
                link = False
            )

def generate(progress=gr.Progress(track_tqdm=True)):
    area_type = 'VIEW_3D' 
    areas  = get_areas(area_type)
    with bpy.context.temp_override(window=window, area=areas[0], region=get_regions(areas)[0], screen=screen):

        for obj in bpy.context.scene.objects:
            if obj.type == 'MESH':
                bpy.data.objects.remove(obj, do_unlink=True)

        molecule = mn.load.molecule_rcsb("7TYG", starting_style=style, center_molecule=True) # <- this line would causes the error 🐛
        molecule.select_set(True)
        bpy.context.view_layer.objects.active = molecule

        bpy.ops.view3d.camera_to_view_selected()
        camera = bpy.data.objects["Camera"]
        camera.data.dof.use_dof = True
        camera.data.dof.focus_distance = 5
        camera.data.dof.aperture_fstop = 4
        camera.data.angle = pi / 3
        camera.data.type = "PERSP"

        with tempfile.NamedTemporaryFile(suffix=".JPEG", delete=False) as f:
            bpy.context.scene.render.resolution_y = 1000
            bpy.context.scene.render.resolution_x = 1000
            bpy.context.scene.render.image_settings.file_format = "JPEG"
            bpy.context.scene.render.filepath = f.name

            with tqdm() as pbar:

                def elapsed(dummy):
                    pbar.update()

                bpy.app.handlers.render_stats.append(elapsed)
                bpy.context.scene.frame_set(1)
                bpy.context.scene.frame_current = 1
                bpy.ops.render.render(animation=False, write_still=True)

                bpy.data.images["Render Result"].save_render(
                    filepath=bpy.context.scene.render.filepath
                )
                bpy.app.handlers.render_stats.clear()
                return f.name

with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column():
            render_btn = gr.Button("Render")
        with gr.Column(scale=3):
            image = gr.Image(type="filepath")

    render_btn.click(
        generate,
        outputs=[image],
    )

demo.queue(concurrency_count=1)
demo.launch(debug=True, inline=True)
BradyAJohnston commented 1 year ago

The rendering is slow for me at least because I'm not paying for GPUs, but it works. This along with a bunch of other issues I want to improve before releasing a new version to pip, but then it should be more streamlined.

radames commented 1 year ago

Very cool @BradyAJohnston! I want to lean more about bpy!, thanks for testing it, please share on social! 👏

kolibril13 commented 1 year ago

Wow, that's great to hear, thanks for making this happen, this is really exciting! 👏 And good news: I've applied for a community Grant to get GPU access at hugging face for this project, and that Grant just got approved 🎉🎉🎉 https://huggingface.co/spaces/kolibril13/blender-with-solara/discussions/1#651d704566df2227d9df0090

image
BradyAJohnston commented 1 year ago

Nice @kolibril13, leave a comment when it's up and running and I'll have a bit of a ply around if I have some time.

kolibril13 commented 1 year ago

it's now up and running here: https://huggingface.co/spaces/sci-blender/blender-with-solara I've transferred the repo from my personal account to a new hugging face organization called sci-blender, and I've invited you two there @BradyAJohnston, @radames (Quick note: The GPU access was not affected by the repo transfer, we still have the nvidia t4 small :) ) So when you accept the invite, you two should be able to directly commit to that project as well.

I've also just tested the script a bit: Locally, on my Mac M1, it takes 1 second to render the image. On hugging face with GPU, it takes 60 seconds to render. so I think the script is currently not taking advantage of the GPU on huggingface.

@radames you wrote the function enable_GPUS() https://github.com/radames/Gradio-Blender-bpy/blob/main/app.py#L11-L31 for your torrus-gradio example.

I removed that function for the current hugging face instance, so probably a new enable_GPUS function can be added to this new repo at https://huggingface.co/spaces/sci-blender/blender-with-solara

radames commented 1 year ago

cool! we can try this, does this render engine make sense @BradyAJohnston ?

def enable_GPUS():
    bpy.data.scenes[0].render.engine = "CYCLES" #"CYCLES"
    # Set the device_type
    bpy.context.preferences.addons[
        "cycles"
    ].preferences.compute_device_type = "CUDA"  # or "OPENCL"

    # Set the device and feature set
    bpy.context.scene.cycles.device = "GPU"

    for scene in bpy.data.scenes:
        scene.cycles.device = "GPU"

    bpy.context.preferences.addons["cycles"].preferences.get_devices()
    print(bpy.context.preferences.addons["cycles"].preferences.compute_device_type)
    for d in bpy.context.preferences.addons["cycles"].preferences.devices:
        d["use"] = True  # Using all devices, include GPU and CPU
        print(d["name"])

enable_GPUS()
radames commented 1 year ago

I've add the code above and it works! it takes about 10s now, It would be great to add some Gradio params, to change the the molecular view

kolibril13 commented 1 year ago

@radames: great to hear that it now take 10 s instead of 60 seconds on hugging face! :) It takes 1 second on a local Mac, so I'm curious if there's an option for even faster rendering on hugging face. I just posted this interest in https://blender.chat/channel/python, maybe someone there has an idea to give us even more GPU boost.

@BradyAJohnston : I just added another commit to the project, and now there's a build error: https://huggingface.co/spaces/sci-blender/blender-with-solara

ERROR: Cannot install -r requirements.txt (line 2) and bpy because these package versions have conflicting dependencies.

The conflict is caused by:
    The user requested bpy
    molecularnodes 2.11.0 depends on bpy<4.0 and >=3.5.0
    The user requested bpy
    molecularnodes 2.10.0 depends on bpy<4.0.0 and >=3.5.0

It was working a few days ago. Any idea what might be going wrong here? EDIT: I've just simplified the example and the error is not there anymore, so this is something we can look at later.

kolibril13 commented 1 year ago

I've now simplified the webapp at https://huggingface.co/spaces/sci-blender/blender-with-solara to this minimal example, so it becomes easier to focus on the GPU incorperation:

import gradio as gr
import bpy
import tempfile

def generate():
    with tempfile.NamedTemporaryFile(suffix=".JPEG", delete=False) as f:
        bpy.context.scene.render.resolution_y = 200
        bpy.context.scene.render.resolution_x = 400
        bpy.context.scene.render.image_settings.file_format = "JPEG"
        bpy.context.scene.render.filepath = f.name
        bpy.ops.render.render(animation=False, write_still=True)
        bpy.data.images["Render Result"].save_render(
            filepath=bpy.context.scene.render.filepath
        )
        bpy.app.handlers.render_stats.clear()
        return f.name

with gr.Blocks() as demo:
    with gr.Row():
        with gr.Column():
            render_btn = gr.Button("Render")
        with gr.Column(scale=3):
            image = gr.Image(type="filepath")

    render_btn.click(
        generate,
        outputs=[image],
    )

demo.queue(concurrency_count=1)
demo.launch(debug=True, inline=True)

image Interestingly, the CPU example renders differently on a mac M1 and on huggingface.

kolibril13 commented 12 months ago

hi @radames , hi @BradyAJohnston,

it's been some time, but now I came back to our molecular nodes as a standalone app at https://huggingface.co/spaces/sci-blender/blender-with-gradio

Unfortunately, we have now an error when we try to install bpy with the nvidia t4 small from the community grant:

image
ERROR: Cannot install -r requirements.txt (line 2) and bpy because these package versions have conflicting dependencies.

The conflict is caused by:
    The user requested bpy
    molecularnodes 2.11.0 depends on bpy<4.0 and >=3.5.0
    The user requested bpy
    molecularnodes 2.10.0 depends on bpy<4.0.0 and >=3.5.0

To fix this you could try to:
1. loosen the range of package versions you've specified
2. remove package versions to allow pip attempt to solve the dependency conflict

I've tried to fix bpy==3.6.0 but that brings that error as well.

Interestingly, the app runs fine on the system without the gpu:

image

any ideas how to build the app with gpu?

radames commented 12 months ago

hi @kolibril13, I'm sorry they've remove the grant if there is no activity, do you have plans to progress it?

kolibril13 commented 12 months ago

hi @radames,

They did not remove the grant! :) I was only comparing CPU basic with the Nvidia T4 small setting. I got to know that the installation of the packages works fine on the CPU basic plan, but I get the "conflicting dependencies" error when I switch to the Nvidia T4 small plan. Any ideas to make this work?