runes

Runes

What are they?

Runes are special python scripts that can be distributed by the Signals & Sorcery registry (aka the VAULT). Runes discovered and run from within a Crucible plugin. They can be hosted locally, on a server, or in Google Colabs. They can be used to execute arbitrary code, or to wrap existing projects.

How do I use a Rune?

There are multiple ways to consume Runes. Lets start with the web plugin.

  1. Open the Crucible Webopen in new window plugin in a browser.
  2. Copy the "token" from the top right of the plugin.
  3. Install the Runes CLI. See [Runes-CLI]/runes-cli/ for details. TLDR: pip install runes-cli
  4. Run the runes-cli in a terminal with the key word runes. Select tokens, then add, then paste the token copied from the plugin.
  5. Run a pre-made Rune. From the runes-cli, select runes (run or manage published runes) then select an available rune from the list. I recommend starting with a rune that does not require a GPU. (look for CPU) Try the Rune Template for starter.
  6. After the Rune has has started, you can interact with if from the Crucible Web plugin. Go back to the plugin and select the Connected Runes tab. You should see the Rune you started. Click on the Rune to interact with it!

Elixir Client

The Elixir client is a Python package that enables you to write python functions, package the functions and expose them to Crucible plugins. It is a simple wrapper around the Signals & Sorcery API.

How do I create a Rune?

Creating your own Rune is easy. The quickest way to get started is by copying the Rune Templateopen in new window. You will see a commented section indicating where to write your custom code. You can write any Python3 code you wish.

Steps to create a custom Rune

Publishing a Rune to the Vault can be divided into 3 steps:

  1. Develop your Rune as a Jupyter Notebook .ipynb file

    • create a directory for your Rune
     mkdir my_rune
    
    • enter the directory
     cd my_rune
    
    pip install notebook
    
    • launch the Jupyter notebook then open the .ipynb file
    jupyter notebook
    
    • customize, test, and save the template. See the Rune section for more details.
  2. Build your .ipynb file into as Rune docker-image using the runes-cli

    • install the RUNES CLI
    • launch the runes-cli using the runes keyword
    • select build rune from jupyter notebook
    • you will be prompted to enter the path to your .ipynb file
    • follow the prompts to build your notebook into a Rune docker-image
  3. Publish the docker-image as a Rune to the Vault using the runes-cli

    • using runes-cli select the newly created docker-image
    • first run the image locally to ensure it works
    • you will be required to have a dockerhub account where the image will be pushed
    • go to DockerHubopen in new window to create an account and remember your credentials
    • now using the runes-cli again select your docker-image and publish the image to the Vault. You will be prompted for metadata about the Rune. Fill in the metadata and the Rune will be published to the Vault!

DANGER

NOTE: The system does not guarantee any security. Do not expose sensitive data in your remotes.

RUNES-CLIENT DOCS

Insallation

pip install runes-client --upgrade

This is a simple example of a RUNES script created using the runes-client. The script defines an arbitrary function that takes two arguments, an integer and a RunesFilePath. The function is registered with the SignalsAndSorceryAPI server. The script then connects to the SignalsAndSorceryAPI server and waits for a plugin to interact with it.

For thorough documentation and tutorials visit: https://signalsandsorcery.com/client/open in new window

import runes_client.core as runes 
from runes_client import RunesFilePath, ui_param

# The token generated by the Crucible plugin.  
# The token is used by the discovery server.  It associates the RUNE with the plugin.
TOKEN="0715c132-0b31-406e-b562-9206c479a48a"

# There are two separate function you need to register.  
# 1. The imports.  This is where you load any libraries you need to use in your RUNE.  This is important so that the RUNE is aware when dependencies are loaded and its ready to be called from the plugin.
# 2. The method.  This is the primary function of the RUNE.  This is where you write the code that will be executed when the RUNE is triggered.

# Name the function anything you like.  This is the imports function.  This is where you load any libraries you need to use in your RUNE.
async def register_imports():
    
    # perform any ```pip install <package>``` here
   
    import uuid, os, random, shutil 

    # all imports need to be made global.  If your imports are straight forward you can use the `make_imports_global` function.
    # If you need to do something more complex you will need to manually make the imports global. For example, in this case we download a large pre-trained model and need to make it global.

    # global model
    # model = MusicGen.get_pretrained('facebook/musicgen-melody')

    runes.make_imports_global([uuid, os, random, shutil])
    
    #NOTE: `make_imports_global()` does not support imports with aliases.  You will need to manually make these global.
    
# Now register the imports function with the discovery server.
runes.register_imports(register_imports)

# The registered method can be named anything.  This is the primary function of the RUNE.  
# This function will be rendered as a web form in the crucible plugin. Note: the method must be `async`.  
# All parameters must be type hinted.  
# Five parameter types are supported: int, float, str, bool, RunesFilePath
# RunesFilePath is a special type. When the file is sent to the remote, it is intercepted by the system and 
# transported to a temp dir on the remote.  In this case the variable `b` is local path to the file.

# The `ui_param` is an optional decorator. It is used to define how the parameter input UI will be rendered in the plugin.  
# If the decorator is not used, the parameter will be rendered as a text input field. 
@ui_param('a', 'RunesNumberSlider', min=0, max=10, step=1, default=5)
@ui_param('c', 'RunesMultiChoice', options=['cherries', 'oranges', 'grapes'], default='grapes')
async def arbitrary_method(a: int, b: RunesFilePath, c: str):
    try: 
        # -----------------------------------------
        # This is where you can write custom code to operate on the input params.
        # ex param `a` could be the number of variations created from param `b` using something like MusicLM
        # -----------------------------------------
        
        # This is how you send results back to the plugin, when processing is complete.
        await runes.results().add_file(b) 
        # This message is displayed in the plugin.
        await runes.results().add_message("This is a message XYZ") 

        return True
    except Exception as e: 
        #explicitly send an error message back to the plugin
        await runes.results().add_error(f"Method encountered an error: {e}")
        return False


# The token generated by the plugin. 
runes.set_token(token=TOKEN)
# The name of the remote.  This is displayed in the plugin.
runes.set_name("My Remote Code")
# The description of the remote.  This is displayed in the plugin.
runes.set_description("This is not a real description.")
# Register the method with the discovery server.
runes.register_method(arbitrary_method)

# When a file is sent to the remote as a RunesFilePath, it will become available at this sample rate. 
runes.set_input_target_sample_rate(44100) #supported values [22050, 32000, 44100, 48000]
# When a file is sent to the remote as a RunesFilePath, it will become available at this bit rate. 
runes.set_input_target_bit_depth(16) #supported values [16, 24, 32]
# When a file is sent to the remote as a RunesFilePath, it will become available with this number of channels.
runes.set_input_target_channels(2) #supported values [1, 2] mono/stereo respectively
# When a file is sent to the remote as a RunesFilePath, it will become available in this format.
runes.set_input_target_format('wav') #supported values ["wav", "mp3", "aif", "flac"]

# When results are sent back to the plugin, they will be sent at this sample rate.
runes.set_output_target_sample_rate(44100)
# When results are sent back to the plugin, they will be sent at this bit rate.
runes.set_output_target_bit_depth(16)
# When results are sent back to the plugin, they will be sent with this number of channels.
runes.set_output_target_channels(2)
# When results are sent back to the plugin, they will be sent in this format.
runes.set_output_target_format('wav')

# This should be the last line of the script.  It connects to the discovery server and waits for a remote trigger.
runes.connect_to_server()
Last Updated:
Contributors: Steve Hiehn, Steve Hiehn