Liang Qu commited on
Commit
f2de1ca
·
1 Parent(s): 6597142

Initial commit.

Browse files
README.md CHANGED
@@ -8,7 +8,68 @@ sdk_version: 5.25.0
8
  app_file: app.py
9
  pinned: false
10
  license: openrail
11
- short_description: Unwritten Chinese Charecters Generation in Style
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  app_file: app.py
9
  pinned: false
10
  license: openrail
11
+ short_description: Unwritten Chinese Charecters in Style
12
  ---
13
 
14
+ # What is this?
15
+
16
+ Generate New Characters by combining parts in creative ways. Write them in a controlled style.
17
+
18
+ - Inspired by
19
+ - Lin Yutang's [Ming-Kwai typewriter](https://en.wikipedia.org/wiki/Chinese_typewriter#MingKwai_design)
20
+ - Wu Yue's [Glyffuser](https://yue-here.com/posts/glyffuser/)
21
+
22
+ # Why
23
+
24
+ - Fun to generate valid but unseen characters. (Never in a dictionary, nor Unicode).
25
+ - Implements Lin Yutang's ideas with AI/ML, without the mechanical marvel :-/ or limitations :-)
26
+ - Extends a font to support new charsets, and beyond to non-existent chars.
27
+ - Adds variation/diversity/personality to generated images. No boring duplicates from the same char.
28
+ - Other [Creative Uses](#creative-uses)
29
+
30
+ # How to use this app
31
+ - Combine components or radicals in the following way
32
+ - Specify the 'Structure' and 'Components', in a [Polish Notation](https://en.wikipedia.org/wiki/Polish_notation) fashion - Good for tree structures
33
+ - ⿰: 'LR' Left-Rigth
34
+ - ⿱: 'TB' Top-Bottom
35
+ - ⿸: 'TL' Top-Left
36
+ - ⿹: 'TR' Top-Right
37
+ - ⿺: 'BL' Bottom-Left
38
+ - ⿴: 'OI' Outer-Inner
39
+ - ⿻: 'OV' Overlap
40
+ - ⿲: 'LMR' Left-Middle-Right
41
+ - ⿳: 'TMB' Top-Middle-Bottom
42
+ - ⿵: 'BT' Bottom Open Enclosure
43
+ - ⿶: 'CT' Top Open Enclosure
44
+ - ⿷: 'RT' Right Open Enclosure
45
+ - Select a 'Style' by clicking the sample images
46
+ - Hit the 'Generate' button
47
+ - Repeat
48
+
49
+ # Usage Tips
50
+ - Simple structures work best (⿰ ⿱ ⿴ etc.)
51
+ - "Known radicals at seen positions" work best (釒on left better than right, but may also surprise you in a good way)
52
+ - Noto font family (sans and serif) gives the best results, as there are many training examples
53
+ - Cursive and handwritten styles usually give good results, as they are more tolerant
54
+ - Fonts supporting less chars are challenging
55
+ - Current model was trained with 300k samples for only 20 epochs
56
+ - Training will continue if this app gets attention or likes
57
+
58
+ - For dictionary chars, [decompose](https://github.com/cburgmer/cjklib/blob/master/cjklib/data/characterdecomposition.csv) first.
59
+ - For a part is hard to describe, or you don't care, use '?' (full-width question mark, or does it matter?)
60
+
61
+ - What to do when the results are not as expected
62
+ - Pick a different 'sytle' which may have trained the model better
63
+ - Try again with a different random seed. This will change the overall structure in an unpredictable way
64
+ - Try again with a different 'step' number. This will change the local details in a continuous way
65
+
66
+ # Creative Uses
67
+ ## Turning a bug into a feature
68
+ When you see a funny result you didn't expect (5 or 3 dots while it should be 4), don't throw it away immediately.
69
+ - Save the results to confuse/train OCR
70
+ - 3vade 3vil c3nsorship
71
+ - Share in discussion. The input text/seed/step will reliably reproduce the result.
72
+
73
+ # Future Features
74
+ - Typewriter keyboard for hard-to-input radicals, filtered by pinyin prefix
75
+ - Direct generation of a single char, auto decomposition%
app.py CHANGED
@@ -1,36 +1,39 @@
1
- import gradio as gr
2
- import numpy as np
3
  import random
 
 
 
 
4
 
5
- # import spaces #[uncomment to use ZeroGPU]
6
  from diffusers import DiffusionPipeline
 
7
  import torch
8
 
 
 
 
9
  device = "cuda" if torch.cuda.is_available() else "cpu"
10
- model_repo_id = "stabilityai/sdxl-turbo" # Replace to the model you would like to use
11
 
12
- if torch.cuda.is_available():
13
- torch_dtype = torch.float16
14
- else:
15
- torch_dtype = torch.float32
16
 
17
- pipe = DiffusionPipeline.from_pretrained(model_repo_id, torch_dtype=torch_dtype)
18
- pipe = pipe.to(device)
19
 
20
- MAX_SEED = np.iinfo(np.int32).max
21
- MAX_IMAGE_SIZE = 1024
22
 
 
 
23
 
24
- # @spaces.GPU #[uncomment to use ZeroGPU]
 
25
  def infer(
26
  prompt,
27
  negative_prompt,
28
  seed,
29
  randomize_seed,
30
- width,
31
- height,
32
- guidance_scale,
33
- num_inference_steps,
34
  progress=gr.Progress(track_tqdm=True),
35
  ):
36
  if randomize_seed:
@@ -39,34 +42,140 @@ def infer(
39
  generator = torch.Generator().manual_seed(seed)
40
 
41
  image = pipe(
42
- prompt=prompt,
43
- negative_prompt=negative_prompt,
44
- guidance_scale=guidance_scale,
45
- num_inference_steps=num_inference_steps,
46
- width=width,
47
- height=height,
48
  generator=generator,
 
49
  ).images[0]
50
 
51
  return image, seed
52
 
53
 
54
  examples = [
55
- "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k",
56
- "An astronaut riding a green horse",
57
- "A delicious ceviche cheesecake slice",
 
 
 
 
 
 
58
  ]
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  css = """
61
  #col-container {
62
  margin: 0 auto;
63
- max-width: 640px;
64
  }
65
  """
66
 
67
  with gr.Blocks(css=css) as demo:
68
  with gr.Column(elem_id="col-container"):
69
- gr.Markdown(" # Text-to-Image Gradio Template")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  with gr.Row():
72
  prompt = gr.Text(
@@ -76,9 +185,14 @@ with gr.Blocks(css=css) as demo:
76
  placeholder="Enter your prompt",
77
  container=False,
78
  )
79
-
80
  run_button = gr.Button("Run", scale=0, variant="primary")
81
 
 
 
 
 
 
 
82
  result = gr.Image(label="Result", show_label=False)
83
 
84
  with gr.Accordion("Advanced Settings", open=False):
@@ -100,40 +214,16 @@ with gr.Blocks(css=css) as demo:
100
  randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
101
 
102
  with gr.Row():
103
- width = gr.Slider(
104
- label="Width",
105
- minimum=256,
106
- maximum=MAX_IMAGE_SIZE,
107
- step=32,
108
- value=1024, # Replace with defaults that work for your model
109
- )
110
-
111
- height = gr.Slider(
112
- label="Height",
113
- minimum=256,
114
- maximum=MAX_IMAGE_SIZE,
115
- step=32,
116
- value=1024, # Replace with defaults that work for your model
117
- )
118
-
119
- with gr.Row():
120
- guidance_scale = gr.Slider(
121
- label="Guidance scale",
122
- minimum=0.0,
123
- maximum=10.0,
124
- step=0.1,
125
- value=0.0, # Replace with defaults that work for your model
126
- )
127
-
128
  num_inference_steps = gr.Slider(
129
  label="Number of inference steps",
130
  minimum=1,
131
- maximum=50,
132
  step=1,
133
- value=2, # Replace with defaults that work for your model
134
  )
135
 
136
  gr.Examples(examples=examples, inputs=[prompt])
 
137
  gr.on(
138
  triggers=[run_button.click, prompt.submit],
139
  fn=infer,
@@ -142,9 +232,6 @@ with gr.Blocks(css=css) as demo:
142
  negative_prompt,
143
  seed,
144
  randomize_seed,
145
- width,
146
- height,
147
- guidance_scale,
148
  num_inference_steps,
149
  ],
150
  outputs=[result, seed],
@@ -152,3 +239,4 @@ with gr.Blocks(css=css) as demo:
152
 
153
  if __name__ == "__main__":
154
  demo.launch()
 
 
1
+ import os
2
+ import re
3
  import random
4
+ import numpy as np
5
+
6
+ # !!! spaces must be imported before torch/CUDA
7
+ import spaces
8
 
9
+ from huggingface_hub import login
10
  from diffusers import DiffusionPipeline
11
+ import gradio as gr
12
  import torch
13
 
14
+ from utils import QPipeline
15
+
16
+
17
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
18
 
19
+ login(token=os.environ["HF_TOKEN"])
20
+ model_repo_id = os.environ["MODEL_ID"]
 
 
21
 
22
+ torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
 
23
 
24
+ pipe = QPipeline.from_pretrained(model_repo_id, torch_dtype=torch_dtype).to(device)
 
25
 
26
+ MAX_SEED = 65535
27
+ MAX_IMAGE_SIZE = 128
28
 
29
+
30
+ @spaces.GPU # Enable ZeroGPU if needed
31
  def infer(
32
  prompt,
33
  negative_prompt,
34
  seed,
35
  randomize_seed,
36
+ num_inference_steps=10,
 
 
 
37
  progress=gr.Progress(track_tqdm=True),
38
  ):
39
  if randomize_seed:
 
42
  generator = torch.Generator().manual_seed(seed)
43
 
44
  image = pipe(
45
+ [prompt],
46
+ batch_size=1,
 
 
 
 
47
  generator=generator,
48
+ num_inference_steps=num_inference_steps
49
  ).images[0]
50
 
51
  return image, seed
52
 
53
 
54
  examples = [
55
+ "Structure: (LR 英). Style: style001",
56
+ "Structure: (TL 广 東). Style: style028",
57
+ "Structure: (TB (LR 禾 魚)). Style: style015",
58
+ "Structure: (TB 敬 音). Style: style013",
59
+ "Structure: (LR 釒 馬). Style: style018",
60
+ "Structure: (BL 走 羽). Style: style022",
61
+ "Structure: (LR 羊 大). Style: style005",
62
+ "Structure: (LR 鹿 孚). Style: style017",
63
+ "Structure: (OI 口 也). Style: style002",
64
  ]
65
 
66
+ # Map style images to style names (use real image files later)
67
+ style_options = {
68
+ "images/style001.png": "style001",
69
+ "images/style002.png": "style002",
70
+ "images/style003.png": "style003",
71
+ "images/style004.png": "style004",
72
+ "images/style005.png": "style005",
73
+ "images/style006.png": "style006",
74
+ "images/style007.png": "style007",
75
+ "images/style008.png": "style008",
76
+ "images/style009.png": "style009",
77
+ "images/style010.png": "style010",
78
+ "images/style011.png": "style011",
79
+ "images/style012.png": "style012",
80
+ "images/style013.png": "style013",
81
+ "images/style014.png": "style014",
82
+ "images/style015.png": "style015",
83
+ # "images/style016.png": "style016", very similar to 002
84
+ "images/style017.png": "style017",
85
+ "images/style018.png": "style018",
86
+ "images/style019.png": "style019",
87
+ "images/style020.png": "style020",
88
+ "images/style021.png": "style021",
89
+ "images/style022.png": "style022",
90
+ "images/style023.png": "style023",
91
+ "images/style024.png": "style024",
92
+ "images/style025.png": "style025",
93
+ "images/style026.png": "style026",
94
+ "images/style027.png": "style027",
95
+ "images/style028.png": "style028",
96
+ "images/style029.png": "style029",
97
+ }
98
+
99
+
100
+ def apply_style_on_click(evt: gr.SelectData, prompt_text):
101
+ index = evt.index
102
+ style_label = list(style_options.values())[index]
103
+
104
+ if re.search(r"Style: [^\n]+", prompt_text):
105
+ return re.sub(r"Style: [^\n]+", f"Style: {style_label}", prompt_text)
106
+ else:
107
+ return prompt_text.strip() + f" Style: {style_label}"
108
+
109
+
110
+ # CSS for fixing Gallery layout
111
  css = """
112
  #col-container {
113
  margin: 0 auto;
114
+ max-width: 800px;
115
  }
116
  """
117
 
118
  with gr.Blocks(css=css) as demo:
119
  with gr.Column(elem_id="col-container"):
120
+ gr.Markdown(" # NeoChar ")
121
+ gr.Markdown(" ## What: Create New Characters. Write them in Style. ")
122
+ gr.Markdown(" * NO more missing glyphs - Make them when fonts don't support! ")
123
+ gr.Markdown(" * Create valid and new Hanzi/Kanji that never existed before ")
124
+ gr.Markdown(" * Calligraphy with diversity - powered by controlled chaos")
125
+ gr.Markdown(" ## How ")
126
+ gr.Markdown(" * Specify 'Structure' and 'Components'. LR: Left-Right, TB: Top-Bottom, etc. ")
127
+ gr.Markdown(" * Select a 'Style' ")
128
+ gr.Markdown(" * (examples below)")
129
+ gr.Markdown(" ## README for more ")
130
+
131
+
132
+ gr.HTML("""
133
+ <style>
134
+ .gallery-container .gallery-item {
135
+ width: 60px !important;
136
+ height: 60px !important;
137
+ padding: 0 !important;
138
+ margin: 4px !important;
139
+ border-radius: 4px;
140
+ overflow: hidden;
141
+ background: none !important;
142
+ box-shadow: none !important;
143
+ }
144
+
145
+ .gallery-container .gallery-item img {
146
+ width: 64px !important;
147
+ height: 64px !important;
148
+ object-fit: cover;
149
+ display: block;
150
+ margin: auto;
151
+ }
152
+
153
+ .gallery-container button {
154
+ all: unset !important;
155
+ padding: 0 !important;
156
+ margin: 0 !important;
157
+ border: none !important;
158
+ background: none !important;
159
+ box-shadow: none !important;
160
+ }
161
+
162
+ .gallery__modal,
163
+ .gallery-container .preview,
164
+ .gallery-container .gallery-item:focus-visible {
165
+ display: none !important;
166
+ pointer-events: none !important;
167
+ }
168
+ </style>
169
+ """)
170
+
171
+ gallery = gr.Gallery(
172
+ value=list(style_options.keys()),
173
+ label="Click any image",
174
+ columns=7,
175
+ allow_preview=False,
176
+ height=None,
177
+ elem_classes=["gallery-container"]
178
+ )
179
 
180
  with gr.Row():
181
  prompt = gr.Text(
 
185
  placeholder="Enter your prompt",
186
  container=False,
187
  )
 
188
  run_button = gr.Button("Run", scale=0, variant="primary")
189
 
190
+ gallery.select(
191
+ fn=apply_style_on_click,
192
+ inputs=[prompt],
193
+ outputs=prompt
194
+ )
195
+
196
  result = gr.Image(label="Result", show_label=False)
197
 
198
  with gr.Accordion("Advanced Settings", open=False):
 
214
  randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
215
 
216
  with gr.Row():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  num_inference_steps = gr.Slider(
218
  label="Number of inference steps",
219
  minimum=1,
220
+ maximum=20,
221
  step=1,
222
+ value=10,
223
  )
224
 
225
  gr.Examples(examples=examples, inputs=[prompt])
226
+
227
  gr.on(
228
  triggers=[run_button.click, prompt.submit],
229
  fn=infer,
 
232
  negative_prompt,
233
  seed,
234
  randomize_seed,
 
 
 
235
  num_inference_steps,
236
  ],
237
  outputs=[result, seed],
 
239
 
240
  if __name__ == "__main__":
241
  demo.launch()
242
+
images/style001.png ADDED
images/style002.png ADDED
images/style003.png ADDED
images/style004.png ADDED
images/style005.png ADDED
images/style006.png ADDED
images/style007.png ADDED
images/style008.png ADDED
images/style009.png ADDED
images/style010.png ADDED
images/style011.png ADDED
images/style012.png ADDED
images/style013.png ADDED
images/style014.png ADDED
images/style015.png ADDED
images/style016.png ADDED
images/style017.png ADDED
images/style018.png ADDED
images/style019.png ADDED
images/style020.png ADDED
images/style021.png ADDED
images/style022.png ADDED
images/style023.png ADDED
images/style024.png ADDED
images/style025.png ADDED
images/style026.png ADDED
images/style027.png ADDED
images/style028.png ADDED
images/style029.png ADDED
requirements.txt CHANGED
@@ -1,6 +1,14 @@
 
1
  accelerate
2
  diffusers
3
  invisible_watermark
4
  torch
5
  transformers
6
- xformers
 
 
 
 
 
 
 
 
1
+ numpy
2
  accelerate
3
  diffusers
4
  invisible_watermark
5
  torch
6
  transformers
7
+ xformers
8
+ sentencepiece
9
+ datasets
10
+ einops
11
+ Pillow
12
+ torchvision
13
+ tqdm
14
+ imwatermark
utils.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import torch
4
+ from torch import nn
5
+ from torch.utils.data import Dataset, DataLoader
6
+ from torchvision import transforms as T
7
+ from PIL import Image as PILImage, ImageDraw, ImageFont
8
+ from imwatermark import WatermarkEncoder
9
+
10
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline, ImagePipelineOutput
11
+ from diffusers.utils.torch_utils import randn_tensor
12
+ from transformers import MT5Tokenizer, MT5EncoderModel
13
+ from typing import List, Optional, Tuple, Union
14
+
15
+ # Determine device and torch dtype
16
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
17
+ torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
18
+
19
+ # Load MT5 tokenizer and encoder (can be replaced with private model + token if needed)
20
+ tokenizer = MT5Tokenizer.from_pretrained("google/mt5-small", use_safetensors=True)
21
+ encoder_model = MT5EncoderModel.from_pretrained("google/mt5-small", use_safetensors=True).to(device=device, dtype=torch_dtype)
22
+ encoder_model.eval()
23
+
24
+ class QPipeline(DiffusionPipeline):
25
+ def __init__(self, unet, scheduler):
26
+ super().__init__()
27
+ self.register_modules(unet=unet, scheduler=scheduler)
28
+
29
+ def add_watermark(self, img: PILImage.Image) -> PILImage.Image:
30
+ # Resize image to 256, as 128 is too small for watermark
31
+ img = img.resize((256, 256), resample=PILImage.BICUBIC)
32
+
33
+ watermark_str = os.getenv("WATERMARK_URL", "hf.co/lqume/new-hanzi")
34
+ encoder = WatermarkEncoder()
35
+ encoder.set_watermark('bytes', watermark_str.encode('utf-8'))
36
+
37
+ # Convert PIL image to NumPy array
38
+ img_np = np.asarray(img.convert("RGB")) # ensure 3-channel RGB
39
+ watermarked_np = encoder.encode(img_np, 'dwtDct')
40
+
41
+ # Convert back to PIL
42
+ return PILImage.fromarray(watermarked_np)
43
+
44
+ @torch.no_grad()
45
+ def __call__(
46
+ self,
47
+ texts: List[str],
48
+ batch_size: int = 1,
49
+ generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
50
+ num_inference_steps: int = 20,
51
+ output_type: Optional[str] = "pil",
52
+ return_dict: bool = True,
53
+ ) -> Union[ImagePipelineOutput, Tuple[List[PILImage.Image]]]:
54
+
55
+ batch_size = len(texts)
56
+
57
+ # Tokenize input text
58
+ tokenized = tokenizer(
59
+ texts,
60
+ return_tensors="pt",
61
+ padding="max_length",
62
+ truncation=True,
63
+ max_length=48
64
+ )
65
+ input_ids = tokenized["input_ids"].to(device=device, dtype=torch.long)
66
+ attention_mask = tokenized["attention_mask"].to(device=device, dtype=torch.long)
67
+
68
+ # Encode to latent space
69
+ encoded = encoder_model.encoder(input_ids=input_ids, attention_mask=attention_mask)
70
+
71
+ # Prepare noise tensor
72
+ if isinstance(self.unet.config.sample_size, int):
73
+ image_shape = (
74
+ batch_size,
75
+ self.unet.config.in_channels,
76
+ self.unet.config.sample_size,
77
+ self.unet.config.sample_size,
78
+ )
79
+ else:
80
+ image_shape = (batch_size, self.unet.config.in_channels, *self.unet.config.sample_size)
81
+
82
+ image = randn_tensor(image_shape, generator=generator, device=self.device, dtype=torch_dtype)
83
+
84
+ # Run denoising loop
85
+ self.scheduler.set_timesteps(num_inference_steps)
86
+
87
+ for timestep in self.progress_bar(self.scheduler.timesteps):
88
+ noise_pred = self.unet(
89
+ image,
90
+ timestep,
91
+ encoder_hidden_states=encoded.last_hidden_state,
92
+ encoder_attention_mask=attention_mask.bool(),
93
+ return_dict=False
94
+ )[0]
95
+
96
+ image = self.scheduler.step(noise_pred, timestep, image, generator=generator, return_dict=False)[0]
97
+
98
+ # Final image post-processing
99
+ image = image.clamp(0, 1).cpu().permute(0, 2, 3, 1).numpy()
100
+ if output_type == "pil":
101
+ image = self.numpy_to_pil(image)
102
+ image = [self.add_watermark(img) for img in image]
103
+
104
+ if not return_dict:
105
+ return (image,)
106
+
107
+ return ImagePipelineOutput(images=image)
108
+