Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
*.so
*.DS_Store
.git
.gitignore
.env
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.9.18
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# syntax=docker/dockerfile:1

FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "web_app.py", "--host", "0.0.0.0", "--port", "3000"]
Binary file added __pycache__/data_frame.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/demo.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/drawing.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/lyrics.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/rnn.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/rnn_cell.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/rnn_ops.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/tf_base_model.cpython-39.pyc
Binary file not shown.
Binary file added __pycache__/tf_utils.cpython-39.pyc
Binary file not shown.
40 changes: 35 additions & 5 deletions demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,19 @@ def __init__(self):
)
self.nn.restore()

def write(self, filename, lines, biases=None, styles=None, stroke_colors=None, stroke_widths=None):
def write(
self,
filename,
lines,
biases=None,
styles=None,
stroke_colors=None,
stroke_widths=None,
alignment="center",
):
valid_char_set = set(drawing.alphabet)
if alignment not in {"left", "center"}:
raise ValueError("Alignment must be either 'left' or 'center'.")
for line_num, line in enumerate(lines):
if len(line) > 75:
raise ValueError(
Expand All @@ -59,7 +70,14 @@ def write(self, filename, lines, biases=None, styles=None, stroke_colors=None, s
)

strokes = self._sample(lines, biases=biases, styles=styles)
self._draw(strokes, lines, filename, stroke_colors=stroke_colors, stroke_widths=stroke_widths)
self._draw(
strokes,
lines,
filename,
stroke_colors=stroke_colors,
stroke_widths=stroke_widths,
alignment=alignment,
)

def _sample(self, lines, biases=None, styles=None):
num_samples = len(lines)
Expand All @@ -74,7 +92,7 @@ def _sample(self, lines, biases=None, styles=None):
if styles is not None:
for i, (cs, style) in enumerate(zip(lines, styles)):
x_p = np.load('styles/style-{}-strokes.npy'.format(style))
c_p = np.load('styles/style-{}-chars.npy'.format(style)).tostring().decode('utf-8')
c_p = np.load('styles/style-{}-chars.npy'.format(style)).tobytes().decode('utf-8')

c_p = str(c_p) + " " + cs
c_p = drawing.encode_ascii(c_p)
Expand Down Expand Up @@ -107,7 +125,15 @@ def _sample(self, lines, biases=None, styles=None):
samples = [sample[~np.all(sample == 0.0, axis=1)] for sample in samples]
return samples

def _draw(self, strokes, lines, filename, stroke_colors=None, stroke_widths=None):
def _draw(
self,
strokes,
lines,
filename,
stroke_colors=None,
stroke_widths=None,
alignment="center",
):
stroke_colors = stroke_colors or ['black']*len(lines)
stroke_widths = stroke_widths or [2]*len(lines)

Expand All @@ -133,7 +159,11 @@ def _draw(self, strokes, lines, filename, stroke_colors=None, stroke_widths=None

strokes[:, 1] *= -1
strokes[:, :2] -= strokes[:, :2].min() + initial_coord
strokes[:, 0] += (view_width - strokes[:, 0].max()) / 2
if alignment == "center":
strokes[:, 0] += (view_width - strokes[:, 0].max()) / 2
else:
left_padding = 60
strokes[:, 0] += left_padding

prev_eos = 1.0
p = "M{},{} ".format(0, 0)
Expand Down
26 changes: 26 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,34 @@ hand.write(

Currently, the `Hand` class must be imported from `demo.py`. If someone would like to package this project to make it more usable, please [contribute](#contribute).

### Web UI

A lightweight Flask UI is available to generate SVG files directly from a browser. Install the dependencies and launch the
development server:

```
pip install -r requirements.txt
python web_app.py
```

Open <http://localhost:5000> and enter the text (multi-line is supported) you would like to render. Submitting the form downloads the generated SVG file.

Use `python web_app.py --help` to see options for changing the host/port or enabling debug mode when you need Flask's auto
reloader.

A pretrained model is included, but if you'd like to train your own, read <a href='https://github.com/sjvasquez/handwriting-synthesis/tree/master/data/raw'>these instructions</a>.

### Docker

You can also run the web UI in a container:

```bash
docker build -t handwriting-synthesis .
docker run --rm -p 5000:5000 handwriting-synthesis
```

Open <http://localhost:5000> after the container starts to use the interface.

## Demonstrations
Below are a few hundred samples from the model, including some samples demonstrating the effect of priming and biasing the model. Loosely speaking, biasing controls the neatness of the samples and priming controls the style of the samples. The code for these demonstrations can be found in `demo.py`.

Expand Down
16 changes: 10 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
matplotlib>=2.1.0
pandas>= 0.22.0
scikit-learn>=0.19.1
scipy>=1.0.0
svgwrite>=1.1.12
tensorflow==1.6.0
matplotlib>=3.8.4
numpy>=1.26.4
pandas>=2.2.2
scikit-learn>=1.4.2
scipy>=1.11.4
svgwrite>=1.4.3
tensorflow>=2.20.0
tensorflow-probability>=0.25.0
tf-keras>=2.20.0
flask>=3.0.0
5 changes: 4 additions & 1 deletion rnn.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os

import numpy as np
import tensorflow as tf
import tensorflow.compat.v1 as tf

import drawing
from data_frame import DataFrame
Expand All @@ -12,6 +12,9 @@
from tf_utils import time_distributed_dense_layer


tf.disable_v2_behavior()


class DataReader(object):

def __init__(self, data_dir):
Expand Down
21 changes: 14 additions & 7 deletions rnn_cell.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from collections import namedtuple

import tensorflow as tf
import tensorflow.contrib.distributions as tfd
import numpy as np
import tensorflow.compat.v1 as tf
import tensorflow_probability as tfp
from tensorflow.python.ops.rnn_cell_impl import LSTMCell as TFCompatLSTMCell
from tensorflow.python.ops.rnn_cell_impl import RNNCell as TFCompatRNNCell

from tf_utils import dense_layer, shape

Expand All @@ -13,7 +15,12 @@
)


class LSTMAttentionCell(tf.nn.rnn_cell.RNNCell):
tf.disable_v2_behavior()

tfd = tfp.distributions


class LSTMAttentionCell(TFCompatRNNCell):

def __init__(
self,
Expand Down Expand Up @@ -77,7 +84,7 @@ def __call__(self, inputs, state, scope=None):

# lstm 1
s1_in = tf.concat([state.w, inputs], axis=1)
cell1 = tf.contrib.rnn.LSTMCell(self.lstm_size)
cell1 = TFCompatLSTMCell(self.lstm_size)
s1_out, s1_state = cell1(s1_in, state=(state.c1, state.h1))

# attention
Expand All @@ -101,12 +108,12 @@ def __call__(self, inputs, state, scope=None):

# lstm 2
s2_in = tf.concat([inputs, s1_out, w], axis=1)
cell2 = tf.contrib.rnn.LSTMCell(self.lstm_size)
cell2 = TFCompatLSTMCell(self.lstm_size)
s2_out, s2_state = cell2(s2_in, state=(state.c2, state.h2))

# lstm 3
s3_in = tf.concat([inputs, s2_out, w], axis=1)
cell3 = tf.contrib.rnn.LSTMCell(self.lstm_size)
cell3 = TFCompatLSTMCell(self.lstm_size)
s3_out, s3_state = cell3(s3_in, state=(state.c3, state.h3))

new_state = LSTMAttentionCellState(
Expand Down Expand Up @@ -155,7 +162,7 @@ def termination_condition(self, state):
past_final_char = char_idx >= self.attention_values_lengths
output = self.output_function(state)
es = tf.cast(output[:, 2], tf.int32)
is_eos = tf.equal(es, np.ones_like(es))
is_eos = tf.equal(es, tf.ones_like(es))
return tf.logical_or(tf.logical_and(final_char, is_eos), past_final_char)

def _parse_parameters(self, gmm_params, eps=1e-8, sigma_eps=1e-4):
Expand Down
26 changes: 19 additions & 7 deletions rnn_ops.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import tensorflow.compat.v1 as tf
from tensorflow.python.framework import constant_op
from tensorflow.python.framework import dtypes
from tensorflow.python.framework import ops
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import control_flow_ops
from tensorflow.python.ops import math_ops
from tensorflow.python.ops import tensor_array_ops
from tensorflow.python.ops import variable_scope as vs
from tensorflow.python.ops.rnn_cell_impl import _concat, _like_rnncell
from tensorflow.python.ops.rnn_cell_impl import _concat
from tensorflow.python.ops.rnn import _maybe_tensor_shape_from_tensor
from tensorflow.python.util import nest
from tensorflow.python.framework import tensor_shape
from tensorflow.python.eager import context

tf.disable_v2_behavior()


def _is_rnn_cell(cell):
required_attrs = ("state_size", "output_size")
return all(hasattr(cell, attr) for attr in required_attrs) and callable(cell)


def raw_rnn(cell, loop_fn, parallel_iterations=None, swap_memory=False, scope=None):
"""
Expand All @@ -26,7 +33,7 @@ def raw_rnn(cell, loop_fn, parallel_iterations=None, swap_memory=False, scope=No
final cell state,
)
"""
if not _like_rnncell(cell):
if not _is_rnn_cell(cell):
raise TypeError("cell must be an instance of RNNCell")
if not callable(loop_fn):
raise TypeError("loop_fn must be a callable")
Expand All @@ -37,7 +44,12 @@ def raw_rnn(cell, loop_fn, parallel_iterations=None, swap_memory=False, scope=No
# determined by the parent scope, or is set to place the cached
# Variable using the same placement as for the rest of the RNN.
with vs.variable_scope(scope or "rnn") as varscope:
if context.in_graph_mode():
in_graph_mode_fn = getattr(context, "in_graph_mode", None)
if in_graph_mode_fn is not None:
in_graph_mode = in_graph_mode_fn()
else:
in_graph_mode = not context.executing_eagerly()
if in_graph_mode:
if varscope.caching_device is None:
varscope.set_caching_device(lambda op: op.device)

Expand Down Expand Up @@ -158,7 +170,7 @@ def copy_fn(cur_i, cand_i):
return (next_time, elements_finished, next_input, state_ta,
emit_ta, next_state, loop_state)

returned = control_flow_ops.while_loop(
returned = tf.while_loop(
condition, body, loop_vars=[
time, elements_finished, next_input, state_ta,
emit_ta, state, loop_state],
Expand Down Expand Up @@ -195,7 +207,7 @@ def loop_fn(time, cell_output, cell_state, loop_state):
elements_finished = time >= sequence_length
finished = math_ops.reduce_all(elements_finished)

next_input = control_flow_ops.cond(
next_input = tf.cond(
finished,
lambda: array_ops.zeros([array_ops.shape(inputs)[1], inputs.shape.as_list()[2]], dtype=dtypes.float32),
lambda: inputs_ta.read(time)
Expand Down Expand Up @@ -233,7 +245,7 @@ def loop_fn(time, cell_output, cell_state, loop_state):
)
finished = math_ops.reduce_all(elements_finished)

next_input = control_flow_ops.cond(
next_input = tf.cond(
finished,
lambda: array_ops.zeros_like(initial_input),
lambda: initial_input if cell_output is None else cell.output_function(next_cell_state)
Expand Down
5 changes: 4 additions & 1 deletion tf_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
import time

import numpy as np
import tensorflow as tf
import tensorflow.compat.v1 as tf

from tf_utils import shape


tf.disable_v2_behavior()


class TFBaseModel(object):

"""Interface containing some boilerplate code for training tensorflow models.
Expand Down
14 changes: 9 additions & 5 deletions tf_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import tensorflow as tf
import tensorflow.compat.v1 as tf

tf.disable_v2_behavior()

from tensorflow.keras import initializers


def dense_layer(inputs, output_units, bias=True, activation=None, batch_norm=None,
Expand All @@ -17,7 +21,7 @@ def dense_layer(inputs, output_units, bias=True, activation=None, batch_norm=Non
with tf.variable_scope(scope, reuse=reuse):
W = tf.get_variable(
name='weights',
initializer=tf.contrib.layers.variance_scaling_initializer(),
initializer=initializers.VarianceScaling(),
shape=[shape(inputs, -1), output_units]
)
z = tf.matmul(inputs, W)
Expand All @@ -33,7 +37,7 @@ def dense_layer(inputs, output_units, bias=True, activation=None, batch_norm=Non
z = tf.layers.batch_normalization(z, training=batch_norm, reuse=reuse)

z = activation(z) if activation else z
z = tf.nn.dropout(z, dropout) if dropout is not None else z
z = tf.nn.dropout(z, keep_prob=dropout) if dropout is not None else z
return z


Expand All @@ -57,7 +61,7 @@ def time_distributed_dense_layer(
with tf.variable_scope(scope, reuse=reuse):
W = tf.get_variable(
name='weights',
initializer=tf.contrib.layers.variance_scaling_initializer(),
initializer=initializers.VarianceScaling(),
shape=[shape(inputs, -1), output_units]
)
z = tf.einsum('ijk,kl->ijl', inputs, W)
Expand All @@ -73,7 +77,7 @@ def time_distributed_dense_layer(
z = tf.layers.batch_normalization(z, training=batch_norm, reuse=reuse)

z = activation(z) if activation else z
z = tf.nn.dropout(z, dropout) if dropout is not None else z
z = tf.nn.dropout(z, keep_prob=dropout) if dropout is not None else z
return z


Expand Down
Loading