Story Graph

Story Graph#

A story graph is represented by Nodes which are connected via Edges. Each Node contains a script which consists of Cells. These cells allow to control what happens on the script and to which Node we jump. Therefore a story is represented by a graph.

Graph#

../_images/story_graph.svg

Model graph for the story graph app.#

Engine#

class story_graph.engine.Engine(graph, stream, raise_exceptions=False, run_cleanup_procedure=None)[source]#

An engine executes a Graph for a given StreamPoint. Executing means to iterate over the Node and executing each ScriptCell within such a node.

The engine runs in an async manner so it is possible to do awaits without blocking the server, which means execution is halted until a specific condition is met.

Parameters:
  • graph (Graph) – The graph to execute

  • stream (Stream) – The stream where the graph should be executed on

  • raise_exceptions (bool) – Decides if an exception within e.g. a Python script cell can bring down the execution or if it ignores it but logs it. Defaults to False so an invalid Python script cell does not stop the whole graph.

  • run_cleanup_procedure (Optional[bool]) – If True it executes CmdPeriod.run on the SuperCollider server in order to clear all running sounds, patterns and any left running tasks, creating a clean environment. The default is None which will derive the necessary action based if there are already users on the stream (in which case no reset will be executed).

__init__(graph, stream, raise_exceptions=False, run_cleanup_procedure=None)[source]#
async execute_audio_cell(audio_cell)[source]#

Plays the associated AudioFile of an AudioCell.

Todo

This does not respect the different Playback formats

Return type:

AsyncGenerator[StreamInstruction, None]

async execute_markdown_code(cell_code)[source]#

Runs the code of a markdown cell by parsing its content with the GencasterRenderer.

async execute_node(node, blocking_sleep_time=10000)[source]#

Executes all ScriptCell of a given Node.

Return type:

AsyncGenerator[Union[StreamInstruction, Dialog], None]

async execute_python_cell(cell_code)[source]#

Executes a python ScriptCell. A python cell is run as an async generator, which allows to not just run synchronous code but also asynchronous mode.

It is possible to yield immediate results from this. Currently only the yielding of a Dialog instance is possible, but this could be extended.

In order to secure at least a little bit the execution within such a script cell everything that is a available for execution needs to be stated explicitly here.

Return type:

AsyncGenerator[Dialog, None]

async execute_sc_code(cell_code)[source]#

Executes a SuperCollider code cell

Return type:

AsyncGenerator[StreamInstruction, None]

static get_engine_global_vars(runtime_values=None)[source]#

Generates the dictionary which contains all objects which are available for the execution engine of the graph. This acts as a security measurement.

Important

If anything is changed here please execute

make engine-variables-json

which will create an updated autocomplete JSON for the editor.

Parameters:

runtime_values (Optional[Dict[str, Any]]) –

Allows to add additional objects to the module namespace at runtime. These are injected within execute_python_cell() and consist of

Runtime vars#

key

value

info

loop

loop

the current asyncio loop - can be used to execute additional async code

vars

a dictionary of all stream variables

See get_stream_variables()

self

Current Engine instance

get_stream_variables

Callable

See get_stream_variables()

wait_for_stream_variable

Callable

See wait_for_stream_variable()

Return type:

Dict[str, Dict[str, Any]]

async get_next_node()[source]#

Iterates over each exit NodeDoor of the current node and evaluates its boolean value and decides.

If the node door code consists of invalid code it will be skipped. If all boolean evaluations result in False or invalid code, the default exit will be used.

If multiple out-going edges are connected to an active door, a random edge will be picked to follow for the next node.

If the node does not have any out-going edges a GraphDeadEnd exception will be raised.

Return type:

Node

async get_stream_variables()[source]#

Returns the associated StreamVariable within this Stream session.

Todo

Could be a @property but this can be difficult in async contexts so we use explicit async via a getter method.

Return type:

Dict[str, str]

async start(max_steps=1000)[source]#

Starts the execution of the engine. This method is an async generator which eithor yields a StreamInstruction or a Dialog.

Note

In order to avoid a clumping of the database a lay off period of 0.1 seconds is added between jumping nodes.

Return type:

AsyncGenerator[Union[StreamInstruction, Dialog, GraphDeadEnd], None]

async wait_for_stream_variable(name, timeout=100.0, update_speed=0.5)[source]#

Waits for a stream variable to be set. If the variable was not found/set within the time period of timeout this function will raise the exception ScriptCellTimeout.

Danger

Within a script cell it is necessary to await this async function

await wait_for_stream_variable('start')
exception story_graph.engine.GraphDeadEnd[source]#
exception story_graph.engine.InvalidPythonCode[source]#
exception story_graph.engine.ScriptCellTimeout[source]#

Markdown parser#

A ScriptCell can hold markdown content in our own markdown dialect to control breaks, change of speakers and emphasis but also allows us to access variables which are defined within a stream.models.Stream. In the end this will be transformed into SSML which will be used for stream.models.TextToSpeech

Choosing markdown as a scripting language has been made because it still can be written easily by humans and treats written text as first class citizen.

The dialect is described in GencasterRenderer.

Use md_to_ssml() to convert markdown text within a Python context.

class story_graph.markdown_parser.GencasterRenderer(stream_variables=None)[source]#

Acts as a python parser for the Gencaster markdown dialect.

__init__(stream_variables=None)[source]#
add_break(text)[source]#

Adds a break between words, see break in GC docs.

Example: Add a break of 300ms between hello and world.

hello {break}`300ms` world
Return type:

str

chars(text)[source]#

Speaks surrounded words as characters, so “can” becomes “C A N”, see say as in GC docs.

how {chars}`can` you talk
Return type:

str

eval_python(text)[source]#

Execute a python inline script via eval, e.g.

two plus two is {eval_python}`2+2`

will result in two plus two is 4.

Eval does not allow for variable assignment but we obtain a return value.

Todo

Store variables in story_graph.models.GraphSession context.

Return type:

str

exec_python(text)[source]#

Executes a Python statement which allows to assign variables.

{exec_python}`a=2`
A is now {eval_python}`a`.

becomes A is now 2.

See also

Use var() to access stream variables.

Return type:

str

female(text)[source]#

Speaks as DE_STANDARD_A__FEMALE from stream.models.TextToSpeech.VoiceNameChoices.

hello {female}`world`
Return type:

str

male(text)[source]#

Speaks as DE_STANDARD_B__MALE from stream.models.TextToSpeech.VoiceNameChoices.

hello {male}`world`
Return type:

str

moderate(text)[source]#

Speaks surrounded words in a moderate manner, see emphasis in GC docs.

speak {moderate}`something` to me
Return type:

str

raw_ssml(text)[source]#

Allows to use raw ssml statements to extend functionality that may not be covered by this parser.

Example:

Hello {raw_ssml}`<emphasis level="moderate">world</emphasis>`
Return type:

str

validate_gencaster_tokens(text)[source]#

Validates if the used tags are known to Gencaster

Todo

this is not implemented yet and will raise an exception

Return type:

bool

var(text)[source]#

Refers to the value of a StreamVariable.

Example:

Assuming we have set a streaming variable {"foo": "world"}

Hello {var}`foo`

becomes Hello World.

If the streaming variable does not exist it will be replaced with an empty string “”, but we can provide a fallback value via |.

Hello {var}`something_unknown|foobar`

becomes Hello foobar if the streaming variable something_unknown does not exist.

Return type:

str

class story_graph.markdown_parser.GencasterToken(match_obj)[source]#
__init__(match_obj)[source]#
story_graph.markdown_parser.md_to_ssml(text, stream_variables=None)[source]#

Converts a md text into SSML.

Parameters:

text (str) – Markdown text

Return type:

str

Models#

class story_graph.models.AudioCell(*args, **kwargs)[source]#

Stores information for playback of static audio files.

Parameters:

Relationship fields:

Parameters:

audio_file (ForeignKey to AudioFile) – Audio file (related name: audio_cells)

Reverse relationships:

Parameters:

script_cell (Reverse OneToOneField from ScriptCell) – The script cell of this audio cell (related name of audio_cell)

class PlaybackChoices(value)[source]#

Different kinds of playback.

Playback types#

Name

Description

SYNC

Plays back an audio file and waits for the playback to finish before continuing the execution of the script cells.

ASYNC

Plays back an audio file and immediately continues the execution of script cells. This is fitting for e.g. background music.

class story_graph.models.CellType(value)[source]#

A ScriptCell can contain different types of code, each with unique functionality.

Both, the database and Engine, implement some specific details according to these types.

Cell types#

Name

Description

Database

Engine

Markdown

Allows to write arbitrary text which will get rendered as an audio file via a text to speech service, see TextToSpeech for conversion and GencasterRenderer for the extended Markdown syntax.

Python

Allows to execute python code via exec() which allows to trigger e.g. Dialogs in the frontend (see Dialog) or calculate or fetch any kind of data and store its value as a StreamVariable.

SuperCollider

Executes sclang code on the associated server. This can be used to control the sonic content on the server.

Comment

Does not get executed, but allows to put comments into the graph.

Audio

Allows to playback static audio files. The instruction will be translated into sclang code and will be executed as such on the associated stream.

class story_graph.models.Edge(*args, **kwargs)[source]#

Connects two Node with each other by using their respective NodeDoor.

Important

It is important to note that an edge flows from out_node_door to in_node_door as we follow the notion from the perspective of a story_graph.models.Node rather than from the edge.

digraph Connection { rank = same; subgraph cluster_node_a { rank = same; label = "NODE_A"; NODE_A [shape=Msquare, label="NODE_A\n\nscript_cell_1\nscript_cell_2"]; subgraph cluster_in_nodes_a { label = "IN_NODES"; in_node_door_a [label="in_node_door"]; } subgraph cluster_out_nodes_a { label = "OUT_NODES"; out_node_door_a_1 [label="out_node_door 1"]; out_node_door_a_2 [label="out_node_door 2"]; } in_node_door_a -> NODE_A [label="DB\nreference"]; {out_node_door_a_1, out_node_door_a_2} -> NODE_A; in_node_door_a -> NODE_A [style=dashed, color=red, fontcolor=red, label="Engine\nProgression"]; NODE_A -> out_node_door_a_1 [style=dashed, color=red]; } edge_ [shape=Msquare, label="EDGE"]; edge_ -> out_node_door_a_1 [label="out_node_door"]; edge_ -> in_node_door_b [label="in_node_door"]; out_node_door_a_1 -> edge_ [style=dashed, color=red]; edge_ -> in_node_door_b [style=dashed, color=red]; subgraph cluster_node_b { rank = same; label = "NODE_B"; NODE_B [shape=Msquare]; subgraph cluster_in_nodes_b { label = "IN_NODES"; in_node_door_b [label="in_node_door"]; } subgraph cluster_out_nodes_b { label = "OUT_NODES"; out_node_door_b_1 [label="out_node_door 1"]; out_node_door_b_2 [label="out_node_door 2"]; } in_node_door_b -> NODE_B; {out_node_door_b_1, out_node_door_b_2} -> NODE_B; in_node_door_b -> NODE_B [style=dashed, color=red]; NODE_B -> out_node_door_b_1 [style=dashed, color=red]; } }

Parameters:

uuid (UUIDField) – Primary key: Uuid

Relationship fields:

Parameters:
save(*args, **kwargs)[source]#

Checks if in_node_door and out_node_door have their respective types in order to avoid any wrong directions within our graph.

class story_graph.models.Graph(*args, **kwargs)[source]#

A collection of Node and Edge. This can be considered a score as well as a program as it has an entry point as a Node and can jump to any other Node, also allowing for recursive loops/cycles.

Each node can be considered a little program on its own which can consist of multiple ScriptCell which can be coded in a variety of languages which can control the frontend and the audio (by e.g. speaking on the stream) or setting a background music.

The story graph is a core concept and can be edited with a native editor.

Parameters:
  • uuid (UUIDField) – Primary key: Uuid

  • name (CharField) – Name. Name of the graph

  • display_name (CharField) – Display name. Will be used as a display name in the frontend

  • slug_name (SlugField) – Slug name. Will be used as a URL

  • stream_assignment_policy (CharField) – Stream assignment policy. Manages the stream assignment for this graph

  • public_visible (BooleanField) – Public visible?. If the graph is not public it will not be listed in the frontend, yet it is still accessible via URL

  • template_name (CharField) – Frontend template. Allows to switch to a different template in the frontend with different connection flows or UI

  • start_text (TextField) – Start text (markdown). Text about the graph which will be displayed at the start of a stream - only if this is set

  • about_text (TextField) – About text (markdown). Text about the graph which can be accessed during a stream - only if this is set

  • end_text (TextField) – End text (markdown). Text which will be displayed at the end of a stream

Reverse relationships:

Parameters:
  • nodes (Reverse ForeignKey from Node) – All nodes of this Graph (related name of graph)

  • stream (Reverse ForeignKey from Stream) – All Streams of this Graph (related name of graph)

class GraphDetailTemplate(value)[source]#

An enumeration.

class StreamAssignmentPolicy(value)[source]#

Each graph can handle different “connection” mechanisms when a listener accesses a graph.

The implementation of each policy is defined in Engine.

StreamAssignmentPolicy

Comment

ONE_GRAPH_ONE_STREAM

All users share the same stream. When the first user visits a graph, a new stream will be set up. Any following user visiting the same story graph stream will be “redirected” to the same stream as long as there is still any user listening to the graph. All users still execute the story graph from the beginning.

ONE_USER_ONE_STREAM

Upon connection, each user will obtain a new and exclusive stream and the graph will be executed upon the stream.

DEACTIVATE

Non functional, for administrative work.

async acreate_entry_node()[source]#

Every graph needs a deterministic, unique entry node which is used to start the iteration over the graph.

The creator of the graph is responsible for calling this method as we can not implicit call it because there are a multitude of ways of creating a Graph (async (asave), sync (save) or in a bulk where we do not have a handle at all).

Return type:

Node

async aget_entry_node()[source]#

See Graph.create_entry_node().

Return type:

Node

class story_graph.models.Node(*args, **kwargs)[source]#

A node.

Parameters:
  • uuid (UUIDField) – Primary key: Uuid

  • name (CharField) – Name. Name of the node

  • color (CharField) – HEX color of the node in graph canvas

  • position_x (FloatField) – Position x. x-Position in graph canvas

  • position_y (FloatField) – Position y. y-Position in graph canvas

  • is_entry_node (BooleanField) – Is Entry node?. Acts as a singular entrypoint for our graph.Only one such node can exist per graph.

  • is_blocking_node (BooleanField) – Is blocking node?. If we encounter this node during graph execution we will halt execution indefinitely on this node. This is useful if we have setup a state and do not want to change it anymore.

Relationship fields:

Parameters:

graph (ForeignKey to Graph) – Graph (related name: nodes)

Reverse relationships:

Parameters:
  • node_doors (Reverse ForeignKey from NodeDoor) – All node doors of this node (related name of node)

  • script_cells (Reverse ForeignKey from ScriptCell) – All script cells of this node (related name of node)

save(*args, **kwargs)[source]#

Save the current instance. Override this in a subclass if you want to control the saving process.

The ‘force_insert’ and ‘force_update’ parameters can be used to insist that the “save” must be an SQL insert or update (or equivalent for non-SQL backends), respectively. Normally, they should not be set.

class story_graph.models.NodeDoor(*args, **kwargs)[source]#

A Node can be entered and exited via multiple paths, where each of these exits and entrances is called a door.

A connection between nodes can only be made via their doors. There are two types of doors:

Door types#

Kind

Description

INPUT

Allows to enter a node. Currently each Node only has one entry point but for future development and a nicer database operations it is also represented.

OUTPUT

Allows to exit a node. After all script cells of a node has been executed, the condition of each door will be evaluated (like in a switch case). Once a condition has been met, the door will be stepped through. This allows to have a visual representation of logic branches.

It is only possible to connect an OUTPUT to an INPUT door via an Edge.

Parameters:

Relationship fields:

Parameters:

node (ForeignKey to Node) – Node (related name: node_doors)

Reverse relationships:

Parameters:
  • in_edges (Reverse ForeignKey from Edge) – All in edges of this Node door (related name of in_node_door)

  • out_edges (Reverse ForeignKey from Edge) – All out edges of this Node door (related name of out_node_door)

class DoorType(value)[source]#

An enumeration.

save(*args, **kwargs)[source]#

Save the current instance. Override this in a subclass if you want to control the saving process.

The ‘force_insert’ and ‘force_update’ parameters can be used to insist that the “save” must be an SQL insert or update (or equivalent for non-SQL backends), respectively. Normally, they should not be set.

exception story_graph.models.NodeDoorMissing[source]#

Exception that can be thrown if a node door is missing. Normally each node should have a default in- and out NodeDoor via a signal, but as this is not forced via the database it is necessary to check for it. In case this check fails, this exception can be raised.

class story_graph.models.ScriptCell(*args, **kwargs)[source]#

Stores a script which can be executed with our Engine on a Stream.

Parameters:

Relationship fields:

Parameters: