TornikeO commited on
Commit
64cf475
·
1 Parent(s): 9e2fe56

Initial commit

Browse files
Files changed (1) hide show
  1. app.py +564 -0
app.py ADDED
@@ -0,0 +1,564 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # Copyright 2017 The TensorFlow Authors. All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ==============================================================================
16
+ """This tool creates an html visualization of a TensorFlow Lite graph.
17
+
18
+ Example usage:
19
+
20
+ python visualize.py foo.tflite foo.html
21
+ """
22
+
23
+ import json
24
+ import os
25
+ import re
26
+ import sys
27
+ import numpy as np
28
+
29
+ # pylint: disable=g-import-not-at-top
30
+ if not os.path.splitext(__file__)[0].endswith(
31
+ os.path.join("tflite_runtime", "visualize")):
32
+ # This file is part of tensorflow package.
33
+ from tensorflow.lite.python import schema_py_generated as schema_fb
34
+ else:
35
+ # This file is part of tflite_runtime package.
36
+ from tflite_runtime import schema_py_generated as schema_fb
37
+ import gradio as gr
38
+ from html import escape
39
+
40
+ # A CSS description for making the visualizer
41
+ # body {font-family: sans-serif; background-color: #fa0;}
42
+ # # font-family: sans-serif;
43
+ """<style>
44
+ table {background-color: #eca;}
45
+ th {background-color: black; color: white;}
46
+ h1 {
47
+ background-color: ffaa00;
48
+ padding:5px;
49
+ color: black;
50
+ }
51
+
52
+ svg {
53
+ margin: 10px;
54
+ border: 2px;
55
+ border-style: solid;
56
+ border-color: black;
57
+ background: white;
58
+ }
59
+
60
+ div {
61
+ border-radius: 5px;
62
+ background-color: #fec;
63
+ padding:5px;
64
+ margin:5px;
65
+ }
66
+
67
+ .tooltip {color: blue;}
68
+ .tooltip .tooltipcontent {
69
+ visibility: hidden;
70
+ color: black;
71
+ background-color: yellow;
72
+ padding: 5px;
73
+ border-radius: 4px;
74
+ position: absolute;
75
+ z-index: 1;
76
+ }
77
+ .tooltip:hover .tooltipcontent {
78
+ visibility: visible;
79
+ }
80
+
81
+ .edges line {
82
+ stroke: #333;
83
+ }
84
+
85
+ text {
86
+ font-weight: bold;
87
+ }
88
+
89
+ .nodes text {
90
+ color: black;
91
+ pointer-events: none;
92
+ font-size: 11px;
93
+ }
94
+ </style>"""
95
+
96
+ _CSS = """
97
+ <script src="https://d3js.org/d3.v4.min.js"></script>
98
+ """
99
+
100
+ _D3_HTML_TEMPLATE = """
101
+ <script>
102
+ function buildGraph() {
103
+ // Build graph data
104
+ var graph = %s;
105
+
106
+ var svg = d3.select("#subgraph%d")
107
+ var width = svg.attr("width");
108
+ var height = svg.attr("height");
109
+ // Make the graph scrollable.
110
+ svg = svg.call(d3.zoom().on("zoom", function() {
111
+ svg.attr("transform", d3.event.transform);
112
+ })).append("g");
113
+
114
+
115
+ var color = d3.scaleOrdinal(d3.schemeDark2);
116
+
117
+ var simulation = d3.forceSimulation()
118
+ .force("link", d3.forceLink().id(function(d) {return d.id;}))
119
+ .force("charge", d3.forceManyBody())
120
+ .force("center", d3.forceCenter(0.5 * width, 0.5 * height));
121
+
122
+ var edge = svg.append("g").attr("class", "edges").selectAll("line")
123
+ .data(graph.edges).enter().append("path").attr("stroke","black").attr("fill","none")
124
+
125
+ // Make the node group
126
+ var node = svg.selectAll(".nodes")
127
+ .data(graph.nodes)
128
+ .enter().append("g")
129
+ .attr("x", function(d){return d.x})
130
+ .attr("y", function(d){return d.y})
131
+ .attr("transform", function(d) {
132
+ return "translate( " + d.x + ", " + d.y + ")"
133
+ })
134
+ .attr("class", "nodes")
135
+ .call(d3.drag()
136
+ .on("start", function(d) {
137
+ if(!d3.event.active) simulation.alphaTarget(1.0).restart();
138
+ d.fx = d.x;d.fy = d.y;
139
+ })
140
+ .on("drag", function(d) {
141
+ d.fx = d3.event.x; d.fy = d3.event.y;
142
+ })
143
+ .on("end", function(d) {
144
+ if (!d3.event.active) simulation.alphaTarget(0);
145
+ d.fx = d.fy = null;
146
+ }));
147
+ // Within the group, draw a box for the node position and text
148
+ // on the side.
149
+
150
+ var node_width = 150;
151
+ var node_height = 30;
152
+
153
+ node.append("rect")
154
+ .attr("r", "5px")
155
+ .attr("width", node_width)
156
+ .attr("height", node_height)
157
+ .attr("rx", function(d) { return d.group == 1 ? 1 : 10; })
158
+ .attr("stroke", "#000000")
159
+ .attr("fill", function(d) { return d.group == 1 ? "#dddddd" : "#000000"; })
160
+ node.append("text")
161
+ .text(function(d) { return d.name; })
162
+ .attr("x", 5)
163
+ .attr("y", 20)
164
+ .attr("fill", function(d) { return d.group == 1 ? "#000000" : "#eeeeee"; })
165
+ // Setup force parameters and update position callback
166
+
167
+
168
+ var node = svg.selectAll(".nodes")
169
+ .data(graph.nodes);
170
+
171
+ // Bind the links
172
+ var name_to_g = {}
173
+ node.each(function(data, index, nodes) {
174
+ console.log(data.id)
175
+ name_to_g[data.id] = this;
176
+ });
177
+
178
+ function proc(w, t) {
179
+ return parseInt(w.getAttribute(t));
180
+ }
181
+ edge.attr("d", function(d) {
182
+ function lerp(t, a, b) {
183
+ return (1.0-t) * a + t * b;
184
+ }
185
+ var x1 = proc(name_to_g[d.source],"x") + node_width /2;
186
+ var y1 = proc(name_to_g[d.source],"y") + node_height;
187
+ var x2 = proc(name_to_g[d.target],"x") + node_width /2;
188
+ var y2 = proc(name_to_g[d.target],"y");
189
+ var s = "M " + x1 + " " + y1
190
+ + " C " + x1 + " " + lerp(.5, y1, y2)
191
+ + " " + x2 + " " + lerp(.5, y1, y2)
192
+ + " " + x2 + " " + y2
193
+ return s;
194
+ });
195
+ }
196
+ console.log("Helllo!");
197
+ buildGraph();
198
+ </script>
199
+ """
200
+
201
+
202
+ def TensorTypeToName(tensor_type):
203
+ """Converts a numerical enum to a readable tensor type."""
204
+ for name, value in schema_fb.TensorType.__dict__.items():
205
+ if value == tensor_type:
206
+ return name
207
+ return None
208
+
209
+
210
+ def BuiltinCodeToName(code):
211
+ """Converts a builtin op code enum to a readable name."""
212
+ for name, value in schema_fb.BuiltinOperator.__dict__.items():
213
+ if value == code:
214
+ return name
215
+ return None
216
+
217
+
218
+ def NameListToString(name_list):
219
+ """Converts a list of integers to the equivalent ASCII string."""
220
+ if isinstance(name_list, str):
221
+ return name_list
222
+ else:
223
+ result = ""
224
+ if name_list is not None:
225
+ for val in name_list:
226
+ result = result + chr(int(val))
227
+ return result
228
+
229
+
230
+ class OpCodeMapper:
231
+ """Maps an opcode index to an op name."""
232
+
233
+ def __init__(self, data):
234
+ self.code_to_name = {}
235
+ for idx, d in enumerate(data["operator_codes"]):
236
+ self.code_to_name[idx] = BuiltinCodeToName(d["builtin_code"])
237
+ if self.code_to_name[idx] == "CUSTOM":
238
+ self.code_to_name[idx] = NameListToString(d["custom_code"])
239
+
240
+ def __call__(self, x):
241
+ if x not in self.code_to_name:
242
+ s = "<UNKNOWN>"
243
+ else:
244
+ s = self.code_to_name[x]
245
+ return "%s (%d)" % (s, x)
246
+
247
+
248
+ class DataSizeMapper:
249
+ """For buffers, report the number of bytes."""
250
+
251
+ def __call__(self, x):
252
+ if x is not None:
253
+ return "%d bytes" % len(x)
254
+ else:
255
+ return "--"
256
+
257
+
258
+ class TensorMapper:
259
+ """Maps a list of tensor indices to a tooltip hoverable indicator of more."""
260
+
261
+ def __init__(self, subgraph_data):
262
+ self.data = subgraph_data
263
+
264
+ def __call__(self, x):
265
+ html = ""
266
+ if x is None:
267
+ return html
268
+
269
+ html += "<span class='tooltip'><span class='tooltipcontent'>"
270
+ for i in x:
271
+ tensor = self.data["tensors"][i]
272
+ html += str(i) + " "
273
+ html += NameListToString(tensor["name"]) + " "
274
+ html += TensorTypeToName(tensor["type"]) + " "
275
+ html += (repr(tensor["shape"]) if "shape" in tensor else "[]")
276
+ html += (repr(tensor["shape_signature"])
277
+ if "shape_signature" in tensor else "[]") + "<br>"
278
+ html += "</span>"
279
+ html += repr(x)
280
+ html += "</span>"
281
+ return html
282
+
283
+
284
+ def GenerateGraph(subgraph_idx, g, opcode_mapper):
285
+ """Produces the HTML required to have a d3 visualization of the dag."""
286
+
287
+ def TensorName(idx):
288
+ return "t%d" % idx
289
+
290
+ def OpName(idx):
291
+ return "o%d" % idx
292
+
293
+ edges = []
294
+ nodes = []
295
+ first = {}
296
+ second = {}
297
+ pixel_mult = 200 # TODO(aselle): multiplier for initial placement
298
+ width_mult = 170 # TODO(aselle): multiplier for initial placement
299
+ for op_index, op in enumerate(g["operators"] or []):
300
+ if op["inputs"] is not None:
301
+ for tensor_input_position, tensor_index in enumerate(op["inputs"]):
302
+ if tensor_index not in first:
303
+ first[tensor_index] = ((op_index - 0.5 + 1) * pixel_mult,
304
+ (tensor_input_position + 1) * width_mult)
305
+ edges.append({
306
+ "source": TensorName(tensor_index),
307
+ "target": OpName(op_index)
308
+ })
309
+ if op["outputs"] is not None:
310
+ for tensor_output_position, tensor_index in enumerate(op["outputs"]):
311
+ if tensor_index not in second:
312
+ second[tensor_index] = ((op_index + 0.5 + 1) * pixel_mult,
313
+ (tensor_output_position + 1) * width_mult)
314
+ edges.append({
315
+ "target": TensorName(tensor_index),
316
+ "source": OpName(op_index)
317
+ })
318
+
319
+ nodes.append({
320
+ "id": OpName(op_index),
321
+ "name": opcode_mapper(op["opcode_index"]),
322
+ "group": 2,
323
+ "x": pixel_mult,
324
+ "y": (op_index + 1) * pixel_mult
325
+ })
326
+ for tensor_index, tensor in enumerate(g["tensors"]):
327
+ initial_y = (
328
+ first[tensor_index] if tensor_index in first else
329
+ second[tensor_index] if tensor_index in second else (0, 0))
330
+
331
+ nodes.append({
332
+ "id": TensorName(tensor_index),
333
+ "name": "%r (%d)" % (getattr(tensor, "shape", []), tensor_index),
334
+ "group": 1,
335
+ "x": initial_y[1],
336
+ "y": initial_y[0]
337
+ })
338
+ graph_str = json.dumps({"nodes": nodes, "edges": edges})
339
+
340
+ html = _D3_HTML_TEMPLATE % (graph_str, subgraph_idx)
341
+ return html
342
+
343
+
344
+ def GenerateTableHtml(items, keys_to_print, display_index=True):
345
+ """Given a list of object values and keys to print, make an HTML table.
346
+
347
+ Args:
348
+ items: Items to print an array of dicts.
349
+ keys_to_print: (key, display_fn). `key` is a key in the object. i.e.
350
+ items[0][key] should exist. display_fn is the mapping function on display.
351
+ i.e. the displayed html cell will have the string returned by
352
+ `mapping_fn(items[0][key])`.
353
+ display_index: add a column which is the index of each row in `items`.
354
+
355
+ Returns:
356
+ An html table.
357
+ """
358
+ html = ""
359
+ # Print the list of items
360
+ html += "<table><tr>\n"
361
+ html += "<tr>\n"
362
+ if display_index:
363
+ html += "<th>index</th>"
364
+ for h, mapper in keys_to_print:
365
+ html += "<th>%s</th>" % h
366
+ html += "</tr>\n"
367
+ for idx, tensor in enumerate(items):
368
+ html += "<tr>\n"
369
+ if display_index:
370
+ html += "<td>%d</td>" % idx
371
+ # print tensor.keys()
372
+ for h, mapper in keys_to_print:
373
+ val = tensor[h] if h in tensor else None
374
+ val = val if mapper is None else mapper(val)
375
+ html += "<td>%s</td>\n" % val
376
+
377
+ html += "</tr>\n"
378
+ html += "</table>\n"
379
+ return html
380
+
381
+
382
+ def CamelCaseToSnakeCase(camel_case_input):
383
+ """Converts an identifier in CamelCase to snake_case."""
384
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_case_input)
385
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
386
+
387
+
388
+ def FlatbufferToDict(fb, preserve_as_numpy):
389
+ """Converts a hierarchy of FB objects into a nested dict.
390
+
391
+ We avoid transforming big parts of the flat buffer into python arrays. This
392
+ speeds conversion from ten minutes to a few seconds on big graphs.
393
+
394
+ Args:
395
+ fb: a flat buffer structure. (i.e. ModelT)
396
+ preserve_as_numpy: true if all downstream np.arrays should be preserved.
397
+ false if all downstream np.array should become python arrays
398
+ Returns:
399
+ A dictionary representing the flatbuffer rather than a flatbuffer object.
400
+ """
401
+ if isinstance(fb, int) or isinstance(fb, float) or isinstance(fb, str):
402
+ return fb
403
+ elif hasattr(fb, "__dict__"):
404
+ result = {}
405
+ for attribute_name in dir(fb):
406
+ attribute = fb.__getattribute__(attribute_name)
407
+ if not callable(attribute) and attribute_name[0] != "_":
408
+ snake_name = CamelCaseToSnakeCase(attribute_name)
409
+ preserve = True if attribute_name == "buffers" else preserve_as_numpy
410
+ result[snake_name] = FlatbufferToDict(attribute, preserve)
411
+ return result
412
+ elif isinstance(fb, np.ndarray):
413
+ return fb if preserve_as_numpy else fb.tolist()
414
+ elif hasattr(fb, "__len__"):
415
+ return [FlatbufferToDict(entry, preserve_as_numpy) for entry in fb]
416
+ else:
417
+ return fb
418
+
419
+
420
+ def CreateDictFromFlatbuffer(buffer_data):
421
+ model_obj = schema_fb.Model.GetRootAsModel(buffer_data, 0)
422
+ model = schema_fb.ModelT.InitFromObj(model_obj)
423
+ return FlatbufferToDict(model, preserve_as_numpy=False)
424
+
425
+
426
+ def create_html(tflite_input, input_is_filepath=True): # pylint: disable=invalid-name
427
+ """Returns html description with the given tflite model.
428
+
429
+ Args:
430
+ tflite_input: TFLite flatbuffer model path or model object.
431
+ input_is_filepath: Tells if tflite_input is a model path or a model object.
432
+
433
+ Returns:
434
+ Dump of the given tflite model in HTML format.
435
+
436
+ Raises:
437
+ RuntimeError: If the input is not valid.
438
+ """
439
+
440
+ # Convert the model into a JSON flatbuffer using flatc (build if doesn't
441
+ # exist.
442
+ if input_is_filepath:
443
+ if not os.path.exists(tflite_input):
444
+ raise RuntimeError("Invalid filename %r" % tflite_input)
445
+ if tflite_input.endswith(".tflite") or tflite_input.endswith(".bin") or tflite_input.endswith(".tf_lite"):
446
+ with open(tflite_input, "rb") as file_handle:
447
+ file_data = bytearray(file_handle.read())
448
+ data = CreateDictFromFlatbuffer(file_data)
449
+ elif tflite_input.endswith(".json"):
450
+ data = json.load(open(tflite_input))
451
+ else:
452
+ raise RuntimeError("Input file was not .tflite or .json")
453
+ else:
454
+ data = CreateDictFromFlatbuffer(tflite_input)
455
+ html = ""
456
+ # html += _CSS
457
+ html += "<h1>TensorFlow Lite Model</h2>"
458
+
459
+ data["filename"] = tflite_input if input_is_filepath else (
460
+ "Null (used model object)") # Avoid special case
461
+
462
+ toplevel_stuff = [("filename", None), ("version", None),
463
+ ("description", None)]
464
+
465
+ html += "<table>\n"
466
+ for key, mapping in toplevel_stuff:
467
+ if not mapping:
468
+ mapping = lambda x: x
469
+ html += "<tr><th>%s</th><td>%s</td></tr>\n" % (key, mapping(data.get(key)))
470
+ html += "</table>\n"
471
+
472
+ # Spec on what keys to display
473
+ buffer_keys_to_display = [("data", DataSizeMapper())]
474
+ operator_keys_to_display = [("builtin_code", BuiltinCodeToName),
475
+ ("custom_code", NameListToString),
476
+ ("version", None)]
477
+
478
+ # Update builtin code fields.
479
+ for d in data["operator_codes"]:
480
+ d["builtin_code"] = max(d["builtin_code"], d["deprecated_builtin_code"])
481
+
482
+ for subgraph_idx, g in enumerate(data["subgraphs"]):
483
+ # Subgraph local specs on what to display
484
+ html += "<div class='subgraph'>"
485
+ tensor_mapper = TensorMapper(g)
486
+ opcode_mapper = OpCodeMapper(data)
487
+ op_keys_to_display = [("inputs", tensor_mapper), ("outputs", tensor_mapper),
488
+ ("builtin_options", None),
489
+ ("opcode_index", opcode_mapper)]
490
+ tensor_keys_to_display = [("name", NameListToString),
491
+ ("type", TensorTypeToName), ("shape", None),
492
+ ("shape_signature", None), ("buffer", None),
493
+ ("quantization", None)]
494
+
495
+ html += "<h2>Subgraph %d</h2>\n" % subgraph_idx
496
+
497
+ # Inputs and outputs.
498
+ html += "<h3>Inputs/Outputs</h3>\n"
499
+ html += GenerateTableHtml([{
500
+ "inputs": g["inputs"],
501
+ "outputs": g["outputs"]
502
+ }], [("inputs", tensor_mapper), ("outputs", tensor_mapper)],
503
+ display_index=False)
504
+
505
+ # Print the tensors.
506
+ html += "<h3>Tensors</h3>\n"
507
+ html += GenerateTableHtml(g["tensors"], tensor_keys_to_display)
508
+
509
+ # Print the ops.
510
+ if g["operators"]:
511
+ html += "<h3>Ops</h3>\n"
512
+ html += GenerateTableHtml(g["operators"], op_keys_to_display)
513
+
514
+ # Visual graph.
515
+ html += "<svg id='subgraph%d' width='1600' height='900'></svg>\n" % (
516
+ subgraph_idx,)
517
+ html += GenerateGraph(subgraph_idx, g, opcode_mapper)
518
+ html += "</div>"
519
+
520
+ # Buffers have no data, but maybe in the future they will
521
+ html += "<h2>Buffers</h2>\n"
522
+ html += GenerateTableHtml(data["buffers"], buffer_keys_to_display)
523
+
524
+ # Operator codes
525
+ html += "<h2>Operator Codes</h2>\n"
526
+ html += GenerateTableHtml(data["operator_codes"], operator_keys_to_display)
527
+
528
+ # html += "</body></html>\n"
529
+
530
+ # return f"<iframe src={escape(html)} ></iframe>"
531
+
532
+ html += """ <script src="https://d3js.org/d3.v4.min.js"></script> """
533
+ return html
534
+
535
+
536
+ def main(argv):
537
+ try:
538
+ tflite_input = argv[1]
539
+ html_output = argv[2]
540
+ except IndexError:
541
+ print("Usage: %s <input tflite> <output html>" % (argv[0]))
542
+ else:
543
+ html = create_html(tflite_input)
544
+ with open(html_output, "w") as output_file:
545
+ output_file.write(html)
546
+
547
+ def process_file(file):
548
+ try:
549
+ html = create_html(file.name)
550
+ return html
551
+ except Exception as e:
552
+ return f"Error: {str(e)}"
553
+
554
+ with gr.Blocks(head=_CSS, ) as demo:
555
+ gr.Markdown("## TensorFlow Lite Model Visualizer")
556
+ file_input = gr.File(label="Upload TFLite File")
557
+ html_output = gr.HTML(label="Generated HTML", container=True)
558
+ file_input.change(process_file, inputs=file_input, outputs=html_output)
559
+
560
+ demo.launch()
561
+
562
+
563
+ # if __name__ == "__main__":
564
+ # main(sys.argv)