radames commited on
Commit
e7568f1
·
1 Parent(s): d7e73f3
Files changed (6) hide show
  1. .gitignore +3 -0
  2. Dockerfile +25 -0
  3. app.py +49 -0
  4. requirements.txt +6 -0
  5. visualblocks/__init__.py +3 -0
  6. visualblocks/server.py +317 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ venv
2
+ __pycache__
3
+ *.py[cod]
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+
7
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
+
9
+ # Set up a new user named "user" with user ID 1000
10
+ RUN useradd -m -u 1000 user
11
+ # Switch to the "user" user
12
+ USER user
13
+ # Set home to the user's home directory
14
+ ENV HOME=/home/user \
15
+ PATH=/home/user/.local/bin:$PATH \
16
+ PYTHONPATH=$HOME/app \
17
+ PYTHONUNBUFFERED=1
18
+
19
+ # Set the working directory to the user's home directory
20
+ WORKDIR $HOME/app
21
+
22
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
23
+ COPY --chown=user . $HOME/app
24
+
25
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import tensorflow as tf
3
+ import tensorflow_hub as hub
4
+ from tensorflow.python.ops.numpy_ops import np_config
5
+ from visualblocks import register_vb_fn, Server
6
+
7
+ np_config.enable_numpy_behavior()
8
+
9
+ hub_handle = "https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2"
10
+ hub_module = hub.load(hub_handle)
11
+
12
+
13
+ # Register the function with visual blocks using the "generic" type (meaning
14
+ # tensors in, tensors out)
15
+ @register_vb_fn(type="generic")
16
+ def styleTransfer(tensors):
17
+ """Inference function for use with Visual Blocks.
18
+
19
+ This function is passed to the Visual Blocks server, which calls it to
20
+ implement a Colab model runner block.
21
+
22
+ Args:
23
+ tensors: A list of np.ndarrays as input tensors. For this particular
24
+ inference function, only the first two np.ndarrays are used. The first
25
+ np.ndarrays is the input content image as a tensor of size [1,
26
+ content_image_height, content_image_width, 3] with floating point pixel
27
+ values ranging from 0 to 1. The second np.ndarrays is the
28
+ input style image as a tensor of size [1, style_image_height,
29
+ style_image_width, 3] with floating point pixel values ranging from 0 to 1.
30
+
31
+ Returns:
32
+ tensors: A list of np.ndarrays as output tensors. For this particular
33
+ inference function, only the first item is used. The first item is the
34
+ output image as a tensor of size [1, height, width, 3] with floating point
35
+ pixel values ranging from 0 to 1.
36
+ """
37
+
38
+ content_tensor = tf.constant(tensors[0], dtype=tf.float32)
39
+ style_tensor = tf.constant(tensors[1], dtype=tf.float32)
40
+ outputs = hub_module(content_tensor, style_tensor)
41
+ stylized_image = outputs[0].numpy()
42
+
43
+ return [
44
+ stylized_image,
45
+ ]
46
+
47
+
48
+ server = Server()
49
+ server.run()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ tensorflow
2
+ numpy
3
+ pandas
4
+ tensorflow_hub
5
+ flask
6
+ portpicker
visualblocks/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """Welcome to Visual Blocks!"""
2
+
3
+ from .server import Server, register_vb_fn
visualblocks/server.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from flask import Flask
3
+ from flask import make_response
4
+ from flask import request
5
+ from flask import send_from_directory
6
+ from typing import Literal
7
+ import json
8
+ import logging
9
+ import numpy as np
10
+ import os
11
+ import portpicker
12
+ import requests
13
+ import shutil
14
+ import sys
15
+ import threading
16
+ import traceback
17
+ import urllib.parse
18
+ import zipfile
19
+
20
+ _VISUAL_BLOCKS_BUNDLE_VERSION = "1683568957"
21
+
22
+ # Disable logging from werkzeug.
23
+ #
24
+ # Without this, flask will show a warning about using dev server (which is OK
25
+ # in our usecase).
26
+ logging.getLogger("werkzeug").disabled = True
27
+
28
+
29
+ # Function registrations.
30
+ GENERIC_FNS = {}
31
+ TEXT_TO_TEXT_FNS = {}
32
+ TEXT_TO_TENSORS_FNS = {}
33
+
34
+
35
+ def register_vb_fn(
36
+ type: Literal["generic", "text_to_text", "text_to_tensors"] = "generic"
37
+ ):
38
+ """A function decorator to register python function with Visual Blocks.
39
+
40
+ Args:
41
+ type:
42
+ the type of function to register for.
43
+
44
+ Currently, VB supports the following function types:
45
+
46
+ generic:
47
+ A function or iterable of functions, defined in the same Colab notebook,
48
+ that Visual Blocks can call to implement a generic model runner block.
49
+
50
+ A generic inference function must take a single argument, the input
51
+ tensors as an iterable of numpy.ndarrays; run inference; and return the
52
+ output tensors, also as an iterable of numpy.ndarrays.
53
+
54
+ text_to_text:
55
+ A function or iterable of functions, defined in the same Colab notebook,
56
+ that Visual Blocks can call to implement a text-to-text model runner
57
+ block.
58
+
59
+ A text_to_text function must take a string and return a string.
60
+
61
+ text_to_tensors:
62
+ A function or iterable of functions, defined in the same Colab notebook,
63
+ that Visual Blocks can call to implement a text-to-tensors model runner
64
+ block.
65
+
66
+ A text_to_tensors function must take a string and return the output
67
+ tensors, as an iterable of numpy.ndarrays.
68
+ """
69
+
70
+ def decorator_register_vb_fn(func):
71
+ func_name = func.__name__
72
+ if type == "generic":
73
+ GENERIC_FNS[func_name] = func
74
+ elif type == "text_to_text":
75
+ TEXT_TO_TEXT_FNS[func_name] = func
76
+ elif type == "text_to_tensors":
77
+ TEXT_TO_TENSORS_FNS[func_name] = func
78
+ return func
79
+
80
+ return decorator_register_vb_fn
81
+
82
+
83
+ def _json_to_ndarray(json_tensor):
84
+ """Convert a JSON dictionary from the web app to an np.ndarray."""
85
+ array = np.array(json_tensor["tensorValues"])
86
+ array.shape = json_tensor["tensorShape"]
87
+ return array
88
+
89
+
90
+ def _ndarray_to_json(array):
91
+ """Convert a np.ndarray to the JSON dictionary for the web app."""
92
+ values = array.ravel().tolist()
93
+ shape = array.shape
94
+ return {
95
+ "tensorValues": values,
96
+ "tensorShape": shape,
97
+ }
98
+
99
+
100
+ def _make_json_response(obj):
101
+ body = json.dumps(obj)
102
+ resp = make_response(body)
103
+ resp.headers["Content-Type"] = "application/json"
104
+ return resp
105
+
106
+
107
+ def _ensure_iterable(x):
108
+ """Turn x into an iterable if not already iterable."""
109
+ if x is None:
110
+ return ()
111
+ elif hasattr(x, "__iter__"):
112
+ return x
113
+ else:
114
+ return (x,)
115
+
116
+
117
+ def _add_to_registry(fns, registry):
118
+ """Adds the functions to the given registry (dict)."""
119
+ for fn in fns:
120
+ registry[fn.__name__] = fn
121
+
122
+
123
+ def _is_list_of_nd_array(obj):
124
+ return isinstance(obj, list) and all(isinstance(elem, np.ndarray) for elem in obj)
125
+
126
+
127
+ def Server(
128
+ host="localhost",
129
+ port=7860,
130
+ generic=None,
131
+ text_to_text=None,
132
+ text_to_tensors=None,
133
+ height=900,
134
+ tmp_dir="/tmp",
135
+ read_saved_pipeline=True,
136
+ ):
137
+ """Creates a server that serves visual blocks web app in an iFrame.
138
+
139
+ Other than serving the web app, it will also listen to requests sent from the
140
+ web app at various API end points. Once a request is received, it will use the
141
+ data in the request body to call the corresponding functions that users have
142
+ registered with VB, either through the '@register_vb_fn' decorator, or passed
143
+ in when creating the server.
144
+
145
+ Args:
146
+ generic:
147
+ A function or iterable of functions, defined in the same Colab notebook,
148
+ that Visual Blocks can call to implement a generic model runner block.
149
+
150
+ A generic inference function must take a single argument, the input
151
+ tensors as an iterable of numpy.ndarrays; run inference; and return the output
152
+ tensors, also as an iterable of numpy.ndarrays.
153
+
154
+ text_to_text:
155
+ A function or iterable of functions, defined in the same Colab notebook,
156
+ that Visual Blocks can call to implement a text-to-text model runner
157
+ block.
158
+
159
+ A text_to_text function must take a string and return a string.
160
+
161
+ text_to_tensors:
162
+ A function or iterable of functions, defined in the same Colab notebook,
163
+ that Visual Blocks can call to implement a text-to-tensors model runner
164
+ block.
165
+
166
+ A text_to_tensors function must take a string and return the output
167
+ tensors, as an iterable of numpy.ndarrays.
168
+
169
+ height:
170
+ The height of the embedded iFrame.
171
+
172
+ tmp_dir:
173
+ The tmp dir where the server stores the web app's static resources.
174
+
175
+ read_saved_pipeline:
176
+ Whether to read the saved pipeline in the notebook or not.
177
+ """
178
+
179
+ _add_to_registry(_ensure_iterable(generic), GENERIC_FNS)
180
+ _add_to_registry(_ensure_iterable(text_to_text), TEXT_TO_TEXT_FNS)
181
+ _add_to_registry(_ensure_iterable(text_to_tensors), TEXT_TO_TENSORS_FNS)
182
+
183
+ app = Flask(__name__)
184
+
185
+ # Disable startup messages.
186
+ cli = sys.modules["flask.cli"]
187
+ cli.show_server_banner = lambda *x: None
188
+
189
+ # Prepare tmp dir and log file.
190
+ base_path = tmp_dir + "/visual-blocks-colab"
191
+ if os.path.exists(base_path):
192
+ shutil.rmtree(base_path)
193
+ os.mkdir(base_path)
194
+ log_file_path = base_path + "/log"
195
+ open(log_file_path, "w").close()
196
+
197
+ # Download the zip file that bundles the visual blocks web app.
198
+ bundle_target_path = os.path.join(base_path, "visual_blocks.zip")
199
+ url = (
200
+ "https://storage.googleapis.com/tfweb/rapsai-colab-bundles/visual_blocks_%s.zip"
201
+ % _VISUAL_BLOCKS_BUNDLE_VERSION
202
+ )
203
+ r = requests.get(url)
204
+ with open(bundle_target_path, "wb") as zip_file:
205
+ zip_file.write(r.content)
206
+
207
+ # Unzip it.
208
+ # This will unzip all files to {base_path}/build.
209
+ with zipfile.ZipFile(bundle_target_path, "r") as zip_ref:
210
+ zip_ref.extractall(base_path)
211
+ site_root_path = os.path.join(base_path, "build")
212
+
213
+ def log(msg):
214
+ """Logs the given message to the log file."""
215
+ now = datetime.now()
216
+ dt_string = now.strftime("%d/%m/%Y %H:%M:%S")
217
+ with open(log_file_path, "a") as log_file:
218
+ log_file.write("{}: {}\n".format(dt_string, msg))
219
+
220
+ @app.route("/api/list_inference_functions")
221
+ def list_inference_functions():
222
+ result = {}
223
+ if len(GENERIC_FNS):
224
+ result["generic"] = list(GENERIC_FNS.keys())
225
+ result["generic"].sort()
226
+ if len(TEXT_TO_TEXT_FNS):
227
+ result["text_to_text"] = list(TEXT_TO_TEXT_FNS.keys())
228
+ result["text_to_text"].sort()
229
+ if len(TEXT_TO_TENSORS_FNS):
230
+ result["text_to_tensors"] = list(TEXT_TO_TENSORS_FNS.keys())
231
+ result["text_to_tensors"].sort()
232
+ return _make_json_response(result)
233
+
234
+ # Note: using "/api/..." for POST requests is not allowed.
235
+ @app.route("/apipost/inference", methods=["POST"])
236
+ def inference_generic():
237
+ """Handler for the generic api endpoint."""
238
+ result = {}
239
+ try:
240
+ func_name = request.json["function"]
241
+ inference_fn = GENERIC_FNS[func_name]
242
+ input_tensors = [_json_to_ndarray(x) for x in request.json["tensors"]]
243
+ output_tensors = inference_fn(input_tensors)
244
+ if not _is_list_of_nd_array(output_tensors):
245
+ result = {
246
+ "error": "The returned value from %s is not a list of ndarray"
247
+ % func_name
248
+ }
249
+ else:
250
+ result["tensors"] = [_ndarray_to_json(x) for x in output_tensors]
251
+ except Exception as e:
252
+ msg = "".join(traceback.format_exception(type(e), e, e.__traceback__))
253
+ result = {"error": msg}
254
+ finally:
255
+ return _make_json_response(result)
256
+
257
+ # Note: using "/api/..." for POST requests is not allowed.
258
+ @app.route("/apipost/inference_text_to_text", methods=["POST"])
259
+ def inference_text_to_text():
260
+ """Handler for the text_to_text api endpoint."""
261
+ result = {}
262
+ try:
263
+ func_name = request.json["function"]
264
+ inference_fn = TEXT_TO_TEXT_FNS[func_name]
265
+ text = request.json["text"]
266
+ ret = inference_fn(text)
267
+ if not isinstance(ret, str):
268
+ result = {
269
+ "error": "The returned value from %s is not a string" % func_name
270
+ }
271
+ else:
272
+ result["text"] = ret
273
+ except Exception as e:
274
+ msg = "".join(traceback.format_exception(type(e), e, e.__traceback__))
275
+ result = {"error": msg}
276
+ finally:
277
+ return _make_json_response(result)
278
+
279
+ # Note: using "/api/..." for POST requests is not allowed.
280
+ @app.route("/apipost/inference_text_to_tensors", methods=["POST"])
281
+ def inference_text_to_tensors():
282
+ """Handler for the text_to_tensors api endpoint."""
283
+ result = {}
284
+ try:
285
+ func_name = request.json["function"]
286
+ inference_fn = TEXT_TO_TENSORS_FNS[func_name]
287
+ text = request.json["text"]
288
+ output_tensors = inference_fn(text)
289
+ if not _is_list_of_nd_array(output_tensors):
290
+ result = {
291
+ "error": "The returned value from %s is not a list of ndarray"
292
+ % func_name
293
+ }
294
+ else:
295
+ result["tensors"] = [_ndarray_to_json(x) for x in output_tensors]
296
+ except Exception as e:
297
+ msg = "".join(traceback.format_exception(type(e), e, e.__traceback__))
298
+ result = {"error": msg}
299
+ finally:
300
+ return _make_json_response(result)
301
+
302
+ @app.route("/", defaults={"path": "index.html"})
303
+ @app.route("/<path:path>")
304
+ def get_static(path):
305
+ """Handler for serving static resources."""
306
+ return send_from_directory(site_root_path, path)
307
+
308
+ # Start background server.
309
+ # threading.Thread(target=app.run, kwargs={"host": host, "port": port}).start()
310
+
311
+ # A thin wrapper class for exposing a "display" method.
312
+ class _Server:
313
+ def run(self):
314
+ print("Visual Blocks server started at http://%s:%s" % (host, port))
315
+ app.run(host=host, port=port)
316
+
317
+ return _Server()