Spaces:
Runtime error
Runtime error
Transfer from git - HC
Browse files- CONTRIBUTING.md +35 -0
- README.md +226 -3
- app.py +29 -111
- asset/css/style.css +58 -9
- meta.py +7 -6
- Build Ingredients Vocab.ipynb → notes/Build Ingredients Vocab.ipynb +0 -0
- utils/__init__.py +0 -0
- utils/api.py +26 -0
- utils/draw.py +86 -0
- utils/ext.py +43 -0
- utils/st.py +10 -0
- utils/utils.py +73 -0
CONTRIBUTING.md
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
1. Fork the repository by clicking on the ``Fork`` button on the repository's page. This creates a copy of the code under your GitHub user account.
|
2 |
+
|
3 |
+
2. Clone your fork to your local disk, and add the base repository as a remote.
|
4 |
+
```bash
|
5 |
+
$ git clone git@github.com:<your-GitHub-username>/chef-transformer.git
|
6 |
+
$ cd chef-transformer
|
7 |
+
$ git remote add upstream https://github.com/chef-transformer/chef-transformer.git
|
8 |
+
```
|
9 |
+
|
10 |
+
3. Create a new branch to hold your development changes.
|
11 |
+
```bash
|
12 |
+
$ git checkout -b a-descriptive-name-for-your-changes
|
13 |
+
```
|
14 |
+
|
15 |
+
> NOTE: Do not work on the ``main`` branch.
|
16 |
+
|
17 |
+
4. Set up a development environment by running the following command in a virtual environment.
|
18 |
+
```bash
|
19 |
+
$ pip install -r requirements.txt
|
20 |
+
```
|
21 |
+
|
22 |
+
5. DEVELOP THE CODE
|
23 |
+
|
24 |
+
6. It is a good idea to sync your copy of the code with the original repository regularly. This way you can quickly account for changes.
|
25 |
+
```bash
|
26 |
+
$ git fetch upstream
|
27 |
+
$ git rebase upstream/main
|
28 |
+
```
|
29 |
+
|
30 |
+
7. Push the changes to your account using:
|
31 |
+
```bash
|
32 |
+
$ git push -u origin a-descriptive-name-for-your-changes
|
33 |
+
```
|
34 |
+
|
35 |
+
8. Once you are satisfied (and the checklist above is happy too), go to the webpage of your fork on GitHub. Click on ``Pull Request`` to send your changes to the project maintainers for review.
|
README.md
CHANGED
@@ -8,10 +8,233 @@ app_file: app.py
|
|
8 |
pinned: false
|
9 |
---
|
10 |
|
11 |
-
#
|
|
|
12 |
|
|
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
streamlit run app.py
|
17 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
pinned: false
|
9 |
---
|
10 |
|
11 |
+
# Chef Transformer (T5)
|
12 |
+
> This is part of the [Flax/Jax Community Week](https://discuss.huggingface.co/t/recipe-generation-model/7475), organized by [HuggingFace](https://huggingface.co/) and TPU usage sponsored by Google.
|
13 |
|
14 |
+
Want to give it a try? Then what's the wait, head over to the demo [here](https://share.streamlit.io/chef-transformer/chef-transformer/main/app.py).
|
15 |
|
16 |
+
|
17 |
+
## Team Members
|
18 |
+
- Mehrdad Farahani ([m3hrdadfi](https://huggingface.co/m3hrdadfi))
|
19 |
+
- Kartik Godawat ([dk-crazydiv](https://huggingface.co/dk-crazydiv))
|
20 |
+
- Haswanth Aekula ([hassiahk](https://huggingface.co/hassiahk))
|
21 |
+
- Deepak Pandian ([rays2pix](https://huggingface.co/rays2pix))
|
22 |
+
- Nicholas Broad ([nbroad](https://huggingface.co/nbroad))
|
23 |
+
|
24 |
+
## Dataset
|
25 |
+
|
26 |
+
[RecipeNLG: A Cooking Recipes Dataset for Semi-Structured Text Generation](https://recipenlg.cs.put.poznan.pl/). This dataset contains **2,231,142** cooking recipes (>2 millions) with size of **2.14 GB**. It's processed in more careful way.
|
27 |
+
|
28 |
+
### Example
|
29 |
+
|
30 |
+
```json
|
31 |
+
{
|
32 |
+
"NER": [
|
33 |
+
"oyster crackers",
|
34 |
+
"salad dressing",
|
35 |
+
"lemon pepper",
|
36 |
+
"dill weed",
|
37 |
+
"garlic powder",
|
38 |
+
"salad oil"
|
39 |
+
],
|
40 |
+
"directions": [
|
41 |
+
"Combine salad dressing mix and oil.",
|
42 |
+
"Add dill weed, garlic powder and lemon pepper.",
|
43 |
+
"Pour over crackers; stir to coat.",
|
44 |
+
"Place in warm oven.",
|
45 |
+
"Use very low temperature for 15 to 20 minutes."
|
46 |
+
],
|
47 |
+
"ingredients": [
|
48 |
+
"12 to 16 oz. plain oyster crackers",
|
49 |
+
"1 pkg. Hidden Valley Ranch salad dressing mix",
|
50 |
+
"1/4 tsp. lemon pepper",
|
51 |
+
"1/2 to 1 tsp. dill weed",
|
52 |
+
"1/4 tsp. garlic powder",
|
53 |
+
"3/4 to 1 c. salad oil"
|
54 |
+
],
|
55 |
+
"link": "www.cookbooks.com/Recipe-Details.aspx?id=648947",
|
56 |
+
"source": "Gathered",
|
57 |
+
"title": "Hidden Valley Ranch Oyster Crackers"
|
58 |
+
}
|
59 |
```
|
60 |
+
|
61 |
+
## How To Use
|
62 |
+
|
63 |
+
```bash
|
64 |
+
# Installing requirements
|
65 |
+
pip install transformers
|
66 |
+
```
|
67 |
+
|
68 |
+
```python
|
69 |
+
from transformers import FlaxAutoModelForSeq2SeqLM
|
70 |
+
from transformers import AutoTokenizer
|
71 |
+
|
72 |
+
MODEL_NAME_OR_PATH = "flax-community/t5-recipe-generation"
|
73 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME_OR_PATH, use_fast=True)
|
74 |
+
model = FlaxAutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME_OR_PATH)
|
75 |
+
|
76 |
+
prefix = "items: "
|
77 |
+
# generation_kwargs = {
|
78 |
+
# "max_length": 1024,
|
79 |
+
# "min_length": 128,
|
80 |
+
# "no_repeat_ngram_size": 3,
|
81 |
+
# "do_sample": True,
|
82 |
+
# "top_k": 60,
|
83 |
+
# "top_p": 0.95
|
84 |
+
# }
|
85 |
+
generation_kwargs = {
|
86 |
+
"max_length": 512,
|
87 |
+
"min_length": 64,
|
88 |
+
"no_repeat_ngram_size": 3,
|
89 |
+
"early_stopping": True,
|
90 |
+
"num_beams": 5,
|
91 |
+
"length_penalty": 1.5,
|
92 |
+
}
|
93 |
+
|
94 |
+
special_tokens = tokenizer.all_special_tokens
|
95 |
+
tokens_map = {
|
96 |
+
"<sep>": "--",
|
97 |
+
"<section>": "\n"
|
98 |
+
}
|
99 |
+
def skip_special_tokens(text, special_tokens):
|
100 |
+
for token in special_tokens:
|
101 |
+
text = text.replace(token, "")
|
102 |
+
|
103 |
+
return text
|
104 |
+
|
105 |
+
def target_postprocessing(texts, special_tokens):
|
106 |
+
if not isinstance(texts, list):
|
107 |
+
texts = [texts]
|
108 |
+
|
109 |
+
new_texts = []
|
110 |
+
for text in texts:
|
111 |
+
text = skip_special_tokens(text, special_tokens)
|
112 |
+
|
113 |
+
for k, v in tokens_map.items():
|
114 |
+
text = text.replace(k, v)
|
115 |
+
|
116 |
+
new_texts.append(text)
|
117 |
+
|
118 |
+
return new_texts
|
119 |
+
|
120 |
+
def generation_function(texts):
|
121 |
+
_inputs = texts if isinstance(texts, list) else [texts]
|
122 |
+
inputs = [prefix + inp for inp in _inputs]
|
123 |
+
inputs = tokenizer(
|
124 |
+
inputs,
|
125 |
+
max_length=256,
|
126 |
+
padding="max_length",
|
127 |
+
truncation=True,
|
128 |
+
return_tensors="jax"
|
129 |
+
)
|
130 |
+
|
131 |
+
input_ids = inputs.input_ids
|
132 |
+
attention_mask = inputs.attention_mask
|
133 |
+
|
134 |
+
output_ids = model.generate(
|
135 |
+
input_ids=input_ids,
|
136 |
+
attention_mask=attention_mask,
|
137 |
+
**generation_kwargs
|
138 |
+
)
|
139 |
+
generated = output_ids.sequences
|
140 |
+
generated_recipe = target_postprocessing(
|
141 |
+
tokenizer.batch_decode(generated, skip_special_tokens=False),
|
142 |
+
special_tokens
|
143 |
+
)
|
144 |
+
return generated_recipe
|
145 |
+
```
|
146 |
+
|
147 |
+
```python
|
148 |
+
items = [
|
149 |
+
"macaroni, butter, salt, bacon, milk, flour, pepper, cream corn",
|
150 |
+
"provolone cheese, bacon, bread, ginger"
|
151 |
+
]
|
152 |
+
generated = generation_function(items)
|
153 |
+
for text in generated:
|
154 |
+
sections = text.split("\n")
|
155 |
+
for section in sections:
|
156 |
+
section = section.strip()
|
157 |
+
if section.startswith("title:"):
|
158 |
+
section = section.replace("title:", "")
|
159 |
+
headline = "TITLE"
|
160 |
+
elif section.startswith("ingredients:"):
|
161 |
+
section = section.replace("ingredients:", "")
|
162 |
+
headline = "INGREDIENTS"
|
163 |
+
elif section.startswith("directions:"):
|
164 |
+
section = section.replace("directions:", "")
|
165 |
+
headline = "DIRECTIONS"
|
166 |
+
|
167 |
+
if headline == "TITLE":
|
168 |
+
print(f"[{headline}]: {section.strip().capitalize()}")
|
169 |
+
else:
|
170 |
+
section_info = [f" - {i+1}: {info.strip().capitalize()}" for i, info in enumerate(section.split("--"))]
|
171 |
+
print(f"[{headline}]:")
|
172 |
+
print("\n".join(section_info))
|
173 |
+
|
174 |
+
print("-" * 130)
|
175 |
+
```
|
176 |
+
|
177 |
+
Output:
|
178 |
+
```text
|
179 |
+
[TITLE]: Macaroni and corn
|
180 |
+
[INGREDIENTS]:
|
181 |
+
- 1: 2 c. macaroni
|
182 |
+
- 2: 2 tbsp. butter
|
183 |
+
- 3: 1 tsp. salt
|
184 |
+
- 4: 4 slices bacon
|
185 |
+
- 5: 2 c. milk
|
186 |
+
- 6: 2 tbsp. flour
|
187 |
+
- 7: 1/4 tsp. pepper
|
188 |
+
- 8: 1 can cream corn
|
189 |
+
[DIRECTIONS]:
|
190 |
+
- 1: Cook macaroni in boiling salted water until tender.
|
191 |
+
- 2: Drain.
|
192 |
+
- 3: Melt butter in saucepan.
|
193 |
+
- 4: Blend in flour, salt and pepper.
|
194 |
+
- 5: Add milk all at once.
|
195 |
+
- 6: Cook and stir until thickened and bubbly.
|
196 |
+
- 7: Stir in corn and bacon.
|
197 |
+
- 8: Pour over macaroni and mix well.
|
198 |
+
----------------------------------------------------------------------------------------------------------------------------------
|
199 |
+
[TITLE]: Grilled provolone and bacon sandwich
|
200 |
+
[INGREDIENTS]:
|
201 |
+
- 1: 2 slices provolone cheese
|
202 |
+
- 2: 2 slices bacon
|
203 |
+
- 3: 2 slices sourdough bread
|
204 |
+
- 4: 2 slices pickled ginger
|
205 |
+
[DIRECTIONS]:
|
206 |
+
- 1: Place a slice of provolone cheese on one slice of bread.
|
207 |
+
- 2: Top with a slice of bacon.
|
208 |
+
- 3: Top with a slice of pickled ginger.
|
209 |
+
- 4: Top with the other slice of bread.
|
210 |
+
- 5: Heat a skillet over medium heat.
|
211 |
+
- 6: Place the sandwich in the skillet and cook until the cheese is melted and the bread is golden brown.
|
212 |
+
----------------------------------------------------------------------------------------------------------------------------------
|
213 |
+
```
|
214 |
+
|
215 |
+
## Evaluation
|
216 |
+
|
217 |
+
The following table summarizes the scores obtained by the **Chef Transformer**. Those marked as (*) are the baseline models.
|
218 |
+
|
219 |
+
| Model | WER | COSIM | ROUGE-2 |
|
220 |
+
| :-------------: | :---: | :---: | :-----: |
|
221 |
+
| Recipe1M+ * | 0.786 | 0.589 | - |
|
222 |
+
| RecipeNLG * | 0.751 | 0.666 | - |
|
223 |
+
| ChefTransformer | 0.709 | 0.714 | 0.290 |
|
224 |
+
|
225 |
+
## Streamlit demo
|
226 |
+
|
227 |
+
```bash
|
228 |
streamlit run app.py
|
229 |
+
```
|
230 |
+
|
231 |
+
## Looking to contribute?
|
232 |
+
Then follow the steps mentioned in this [contributing guide](CONTRIBUTING.md) and you are good to go.
|
233 |
+
|
234 |
+
## Copyright
|
235 |
+
|
236 |
+
Special thanks to those who provided these fantastic materials.
|
237 |
+
- [Anatomy](https://www.flaticon.com/free-icon)
|
238 |
+
- [Chef Hat](https://www.vecteezy.com/members/jellyfishwater)
|
239 |
+
- [Moira Nazzari](https://pixabay.com/photos/food-dessert-cake-eggs-butter-3048440/)
|
240 |
+
- [Instagram Post](https://www.freepik.com/free-psd/recipes-ad-social-media-post-template_11520617.htm)
|
app.py
CHANGED
@@ -18,9 +18,15 @@ import textwrap
|
|
18 |
from examples import EXAMPLES
|
19 |
import dummy
|
20 |
import meta
|
21 |
-
from utils import
|
|
|
|
|
|
|
22 |
remote_css,
|
23 |
local_css,
|
|
|
|
|
|
|
24 |
load_image_from_url,
|
25 |
load_image_from_local,
|
26 |
image_to_base64,
|
@@ -28,104 +34,6 @@ from utils import (
|
|
28 |
)
|
29 |
|
30 |
|
31 |
-
def generate_cook_image(query, app_id, app_key):
|
32 |
-
api_url = f"https://api.edamam.com/api/recipes/v2?type=public&q={query}&app_id={app_id}&app_key={app_key}&field=image"
|
33 |
-
|
34 |
-
try:
|
35 |
-
r = requests.get(api_url)
|
36 |
-
if r.status_code != 200:
|
37 |
-
return None
|
38 |
-
|
39 |
-
rj = r.json()
|
40 |
-
if "hits" not in rj or not len(rj["hits"]) > 0:
|
41 |
-
return None
|
42 |
-
|
43 |
-
data = rj["hits"]
|
44 |
-
data = data[random.randint(1, min(5, len(data) - 1))] if len(data) > 1 else data[0]
|
45 |
-
|
46 |
-
if "recipe" not in data or "image" not in data["recipe"]:
|
47 |
-
return None
|
48 |
-
|
49 |
-
image = data["recipe"]["image"]
|
50 |
-
return image
|
51 |
-
except Exception as e:
|
52 |
-
return None
|
53 |
-
|
54 |
-
|
55 |
-
def generate_food_with_logo_image(bg_path, logo_path, food_url, no_food="asset/frame/no_food.png"):
|
56 |
-
bg = Image.open(bg_path)
|
57 |
-
width, height = bg.size
|
58 |
-
|
59 |
-
logo = Image.open(logo_path)
|
60 |
-
logo_width, logo_height, logo_ratio, logo_rb, logo_mb = logo.size + (3, -20, 45)
|
61 |
-
logo_width, logo_height = (logo_width // logo_ratio, logo_height // logo_ratio)
|
62 |
-
logo = logo.resize((logo_width, logo_height))
|
63 |
-
|
64 |
-
food = load_image_from_url(food_url, rgba_mode=True, default_image=no_food)
|
65 |
-
|
66 |
-
food_width, food_height = (300, 300)
|
67 |
-
food = food.resize((food_width, food_height))
|
68 |
-
|
69 |
-
bg.paste(food, (0, 0), food)
|
70 |
-
bg.paste(logo, (width - logo_width - logo_rb, height - logo_height - logo_mb), logo)
|
71 |
-
|
72 |
-
return bg
|
73 |
-
|
74 |
-
|
75 |
-
def generate_recipe_image(
|
76 |
-
recipe_data,
|
77 |
-
bg_path,
|
78 |
-
food_logo_ia,
|
79 |
-
fonts,
|
80 |
-
bg_color="#ffffff"
|
81 |
-
):
|
82 |
-
bg = Image.open(bg_path)
|
83 |
-
bg.paste(food_logo_ia, (50, 50), food_logo_ia)
|
84 |
-
bg_color = Image.new("RGBA", bg.size, bg_color)
|
85 |
-
bg_color.paste(bg, mask=bg)
|
86 |
-
|
87 |
-
im_editable = ImageDraw.Draw(bg_color)
|
88 |
-
im_editable.text(
|
89 |
-
(418, 30),
|
90 |
-
textwrap.fill(recipe_data["title"], 15).replace(" \n", "\n"),
|
91 |
-
(61, 61, 70),
|
92 |
-
font=fonts["title"],
|
93 |
-
)
|
94 |
-
|
95 |
-
im_editable.text(
|
96 |
-
(100, 450),
|
97 |
-
"Ingredients",
|
98 |
-
(61, 61, 70),
|
99 |
-
font=fonts["body_bold"],
|
100 |
-
)
|
101 |
-
ingredients = recipe_data["ingredients"]
|
102 |
-
ingredients = [textwrap.fill(item, 30).replace("\n", "\n ") for item in ingredients]
|
103 |
-
|
104 |
-
im_editable.text(
|
105 |
-
(50, 520),
|
106 |
-
"\n".join([f"- {item}" for item in ingredients]),
|
107 |
-
(61, 61, 70),
|
108 |
-
font=fonts["body"],
|
109 |
-
)
|
110 |
-
|
111 |
-
im_editable.text(
|
112 |
-
(700, 450),
|
113 |
-
"Directions",
|
114 |
-
(61, 61, 70),
|
115 |
-
font=fonts["body_bold"],
|
116 |
-
)
|
117 |
-
|
118 |
-
directions = recipe_data["directions"]
|
119 |
-
directions = [textwrap.fill(item, 70).replace("\n", "\n ") for item in directions]
|
120 |
-
im_editable.text(
|
121 |
-
(430, 520),
|
122 |
-
"\n".join([f"{i + 1}. {item}" for i, item in enumerate(directions)]).strip(),
|
123 |
-
(61, 61, 70),
|
124 |
-
font=fonts["body"],
|
125 |
-
)
|
126 |
-
return bg_color
|
127 |
-
|
128 |
-
|
129 |
class TextGeneration:
|
130 |
def __init__(self):
|
131 |
self.debug = False
|
@@ -215,6 +123,7 @@ class TextGeneration:
|
|
215 |
return frame
|
216 |
|
217 |
def generate(self, items, generation_kwargs):
|
|
|
218 |
recipe = self.dummy_outputs[random.randint(0, len(self.dummy_outputs) - 1)]
|
219 |
|
220 |
if not self.debug:
|
@@ -291,9 +200,10 @@ def main():
|
|
291 |
# else:
|
292 |
# get_random_frame = generator.frames[0]
|
293 |
|
|
|
294 |
local_css("asset/css/style.css")
|
295 |
|
296 |
-
col1, col2 = st.beta_columns([
|
297 |
with col2:
|
298 |
st.image(load_image_from_local("asset/images/chef-transformer-transparent.png"), width=300)
|
299 |
st.markdown(meta.SIDEBAR_INFO, unsafe_allow_html=True)
|
@@ -321,12 +231,13 @@ def main():
|
|
321 |
prompt_box = EXAMPLES[prompt]
|
322 |
|
323 |
items = st.text_area(
|
324 |
-
'Insert your
|
325 |
pure_comma_separation(prompt_box, return_list=False),
|
326 |
)
|
327 |
items = pure_comma_separation(items, return_list=False)
|
328 |
entered_items = st.empty()
|
329 |
-
|
|
|
330 |
|
331 |
st.markdown(
|
332 |
"<hr />",
|
@@ -354,14 +265,21 @@ def main():
|
|
354 |
food_image = generated_recipe["image"]
|
355 |
food_image = load_image_from_url(food_image, rgba_mode=True, default_image=generator.no_food)
|
356 |
food_image = image_to_base64(food_image)
|
357 |
-
|
|
|
|
|
|
|
|
|
|
|
358 |
directions = [textwrap.fill(item, 70).replace("\n", "\n ") for item in
|
359 |
generated_recipe["directions"]]
|
|
|
|
|
360 |
generated_recipe["by"] = chef
|
361 |
|
362 |
-
r1, r2 = st.beta_columns([
|
363 |
|
364 |
-
with
|
365 |
# st.write(st.session_state.get_random_frame)
|
366 |
# if hasattr(st, "session_state"):
|
367 |
# recipe_post = generator.generate_frame(generated_recipe, st.session_state.get_random_frame)
|
@@ -378,21 +296,21 @@ def main():
|
|
378 |
output_format="PNG"
|
379 |
)
|
380 |
|
381 |
-
with
|
382 |
st.markdown(
|
383 |
" ".join([
|
384 |
"<div class='r-text-recipe'>",
|
385 |
"<div class='food-title'>",
|
386 |
f"<img src='{food_image}' />",
|
387 |
-
f"<h2>{title}</h2>",
|
388 |
"</div>",
|
389 |
'<div class="divider"><div class="divider-mask"></div></div>',
|
390 |
-
"<h3>Ingredients</h3>",
|
391 |
-
"<ul class='ingredients-list'>",
|
392 |
" ".join([f'<li>{item}</li>' for item in ingredients]),
|
393 |
"</ul>",
|
394 |
-
"<h3>Directions</h3>",
|
395 |
-
"<ol class='ingredients-list'>",
|
396 |
" ".join([f'<li>{item}</li>' for item in directions]),
|
397 |
"</ol>",
|
398 |
"</div>"
|
|
|
18 |
from examples import EXAMPLES
|
19 |
import dummy
|
20 |
import meta
|
21 |
+
from utils import ext
|
22 |
+
from utils.api import generate_cook_image
|
23 |
+
from utils.draw import generate_food_with_logo_image, generate_recipe_image
|
24 |
+
from utils.st import (
|
25 |
remote_css,
|
26 |
local_css,
|
27 |
+
|
28 |
+
)
|
29 |
+
from utils.utils import (
|
30 |
load_image_from_url,
|
31 |
load_image_from_local,
|
32 |
image_to_base64,
|
|
|
34 |
)
|
35 |
|
36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
class TextGeneration:
|
38 |
def __init__(self):
|
39 |
self.debug = False
|
|
|
123 |
return frame
|
124 |
|
125 |
def generate(self, items, generation_kwargs):
|
126 |
+
recipe = self.dummy_outputs[0]
|
127 |
recipe = self.dummy_outputs[random.randint(0, len(self.dummy_outputs) - 1)]
|
128 |
|
129 |
if not self.debug:
|
|
|
200 |
# else:
|
201 |
# get_random_frame = generator.frames[0]
|
202 |
|
203 |
+
remote_css("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600&family=Poppins:wght@600&display=swap")
|
204 |
local_css("asset/css/style.css")
|
205 |
|
206 |
+
col1, col2 = st.beta_columns([6, 4])
|
207 |
with col2:
|
208 |
st.image(load_image_from_local("asset/images/chef-transformer-transparent.png"), width=300)
|
209 |
st.markdown(meta.SIDEBAR_INFO, unsafe_allow_html=True)
|
|
|
231 |
prompt_box = EXAMPLES[prompt]
|
232 |
|
233 |
items = st.text_area(
|
234 |
+
'Insert your food items here (separated by `,`): ',
|
235 |
pure_comma_separation(prompt_box, return_list=False),
|
236 |
)
|
237 |
items = pure_comma_separation(items, return_list=False)
|
238 |
entered_items = st.empty()
|
239 |
+
|
240 |
+
recipe_button = st.button('Get Recipe!')
|
241 |
|
242 |
st.markdown(
|
243 |
"<hr />",
|
|
|
265 |
food_image = generated_recipe["image"]
|
266 |
food_image = load_image_from_url(food_image, rgba_mode=True, default_image=generator.no_food)
|
267 |
food_image = image_to_base64(food_image)
|
268 |
+
|
269 |
+
ingredients = ext.ingredients(
|
270 |
+
generated_recipe["ingredients"],
|
271 |
+
pure_comma_separation(items, return_list=True)
|
272 |
+
)
|
273 |
+
|
274 |
directions = [textwrap.fill(item, 70).replace("\n", "\n ") for item in
|
275 |
generated_recipe["directions"]]
|
276 |
+
directions = ext.directions(directions)
|
277 |
+
|
278 |
generated_recipe["by"] = chef
|
279 |
|
280 |
+
r1, r2 = st.beta_columns([6, 2])
|
281 |
|
282 |
+
with r2:
|
283 |
# st.write(st.session_state.get_random_frame)
|
284 |
# if hasattr(st, "session_state"):
|
285 |
# recipe_post = generator.generate_frame(generated_recipe, st.session_state.get_random_frame)
|
|
|
296 |
output_format="PNG"
|
297 |
)
|
298 |
|
299 |
+
with r1:
|
300 |
st.markdown(
|
301 |
" ".join([
|
302 |
"<div class='r-text-recipe'>",
|
303 |
"<div class='food-title'>",
|
304 |
f"<img src='{food_image}' />",
|
305 |
+
f"<h2 class='font-title text-bold'>{title}</h2>",
|
306 |
"</div>",
|
307 |
'<div class="divider"><div class="divider-mask"></div></div>',
|
308 |
+
"<h3 class='ingredients font-body text-bold'>Ingredients</h3>",
|
309 |
+
"<ul class='ingredients-list font-body'>",
|
310 |
" ".join([f'<li>{item}</li>' for item in ingredients]),
|
311 |
"</ul>",
|
312 |
+
"<h3 class='directions font-body text-bold'>Directions</h3>",
|
313 |
+
"<ol class='ingredients-list font-body'>",
|
314 |
" ".join([f'<li>{item}</li>' for item in directions]),
|
315 |
"</ol>",
|
316 |
"</div>"
|
asset/css/style.css
CHANGED
@@ -2,6 +2,18 @@ body {
|
|
2 |
background-color: #fff;
|
3 |
}
|
4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
.fullScreenFrame > div {
|
7 |
display: flex;
|
@@ -29,7 +41,7 @@ body {
|
|
29 |
}
|
30 |
.contributors a.contributor {
|
31 |
text-decoration: none;
|
32 |
-
color: #
|
33 |
}
|
34 |
.contributors a.contributor:hover {
|
35 |
text-decoration: underline;
|
@@ -37,17 +49,24 @@ body {
|
|
37 |
|
38 |
.story-box {
|
39 |
overflow-y: scroll;
|
40 |
-
max-height:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
}
|
42 |
|
43 |
.r-text-recipe {
|
44 |
-
padding-left: 30px;
|
45 |
-
margin-left: 10px
|
46 |
-
border-
|
47 |
}
|
48 |
|
49 |
.divider {
|
50 |
-
margin: 5px
|
51 |
width: 400px;
|
52 |
max-width: 100%;
|
53 |
position:relative;
|
@@ -61,23 +80,53 @@ body {
|
|
61 |
.divider-mask:after {
|
62 |
content: '';
|
63 |
display: block;
|
64 |
-
margin: 0 auto;
|
65 |
width: 170px;
|
66 |
height: 0px;
|
67 |
border-bottom: 2px solid #e9a726;
|
68 |
border-radius: 10px;
|
|
|
69 |
}
|
70 |
|
71 |
.r-text-recipe .food-title {
|
72 |
-
text-align:
|
73 |
}
|
74 |
.r-text-recipe .food-title img {
|
75 |
-
max-width:
|
|
|
|
|
|
|
76 |
}
|
77 |
.r-text-recipe .food-title h2 {
|
78 |
}
|
|
|
79 |
.ingredients-list {
|
80 |
columns: 2;
|
81 |
-webkit-columns: 2;
|
82 |
-moz-columns: 2;
|
83 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
background-color: #fff;
|
3 |
}
|
4 |
|
5 |
+
.font-title {
|
6 |
+
font-family: 'Poppins', sans-serif !important;
|
7 |
+
}
|
8 |
+
.font-body {
|
9 |
+
font-family: 'Montserrat', sans-serif !important;
|
10 |
+
}
|
11 |
+
.text-bold {
|
12 |
+
font-weight: normal !important;
|
13 |
+
}
|
14 |
+
.text-bold {
|
15 |
+
font-weight: bold !important;
|
16 |
+
}
|
17 |
|
18 |
.fullScreenFrame > div {
|
19 |
display: flex;
|
|
|
41 |
}
|
42 |
.contributors a.contributor {
|
43 |
text-decoration: none;
|
44 |
+
color: #585858;
|
45 |
}
|
46 |
.contributors a.contributor:hover {
|
47 |
text-decoration: underline;
|
|
|
49 |
|
50 |
.story-box {
|
51 |
overflow-y: scroll;
|
52 |
+
max-height: 240px;
|
53 |
+
}
|
54 |
+
|
55 |
+
.story-box p {
|
56 |
+
font-size: 0.85rem;
|
57 |
+
}
|
58 |
+
.story-box pre {
|
59 |
+
font-size: 0.6rem;
|
60 |
}
|
61 |
|
62 |
.r-text-recipe {
|
63 |
+
/* padding-left: 30px;
|
64 |
+
margin-left: 10px;*/
|
65 |
+
border-right: 1px dashed #eee;
|
66 |
}
|
67 |
|
68 |
.divider {
|
69 |
+
margin: 5px 0;
|
70 |
width: 400px;
|
71 |
max-width: 100%;
|
72 |
position:relative;
|
|
|
80 |
.divider-mask:after {
|
81 |
content: '';
|
82 |
display: block;
|
|
|
83 |
width: 170px;
|
84 |
height: 0px;
|
85 |
border-bottom: 2px solid #e9a726;
|
86 |
border-radius: 10px;
|
87 |
+
left: 0px;
|
88 |
}
|
89 |
|
90 |
.r-text-recipe .food-title {
|
91 |
+
text-align: left;
|
92 |
}
|
93 |
.r-text-recipe .food-title img {
|
94 |
+
max-width: 300px;
|
95 |
+
float: left;
|
96 |
+
margin-right: 30px;
|
97 |
+
margin-bottom: 30px;
|
98 |
}
|
99 |
.r-text-recipe .food-title h2 {
|
100 |
}
|
101 |
+
.ingredients {}
|
102 |
.ingredients-list {
|
103 |
columns: 2;
|
104 |
-webkit-columns: 2;
|
105 |
-moz-columns: 2;
|
106 |
}
|
107 |
+
.directions {
|
108 |
+
clear: both;
|
109 |
+
float: none;
|
110 |
+
padding-top: 20px;
|
111 |
+
display: block;
|
112 |
+
}
|
113 |
+
.directions-list {}
|
114 |
+
|
115 |
+
|
116 |
+
@media only screen and (max-width: 600px) {
|
117 |
+
.r-text-recipe {
|
118 |
+
border-right: 0;
|
119 |
+
border-bottom: 1px dashed #eee;
|
120 |
+
}
|
121 |
+
.r-text-recipe .food-title img {
|
122 |
+
max-width: 200px;
|
123 |
+
}
|
124 |
+
.directions {
|
125 |
+
padding-top: 0px;
|
126 |
+
}
|
127 |
+
.ingredients-list {
|
128 |
+
columns: 1;
|
129 |
+
-webkit-columns: 1;
|
130 |
+
-moz-columns: 1;
|
131 |
+
}
|
132 |
+
}
|
meta.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
HEADER_INFO = """""".strip()
|
2 |
SIDEBAR_INFO = """
|
3 |
-
<div class="contributors">
|
4 |
<a class="contributor comma" href="https://huggingface.co/m3hrdadfi">Mehrdad Farahani</a>
|
5 |
<a class="contributor comma" href="https://huggingface.co/dk-crazydiv">Kartik Godawat</a>
|
6 |
<a class="contributor comma" href="https://huggingface.co/hassiahk">Haswanth Aekula</a>
|
@@ -9,13 +9,15 @@ SIDEBAR_INFO = """
|
|
9 |
</div>
|
10 |
"""
|
11 |
CHEF_INFO = """
|
12 |
-
<
|
|
|
13 |
<span class="d-block extra-info">(We are at your service with two of the best chefs in the world: Chef Scheherazade,
|
14 |
-
Chef Giovanni. Scheherazade is known for being more creative whereas Giovanni is more meticulous.)</span
|
|
|
15 |
""".strip()
|
16 |
PROMPT_BOX = "Add custom ingredients here (separated by `,`): "
|
17 |
STORY = """
|
18 |
-
<div class="story-box">
|
19 |
<p>
|
20 |
Hello everyone 👋, I am <strong>Chef Transformer</strong>,
|
21 |
the owner of this restaurant. I was made by a group of <a href="https://huggingface.co/flax-community/t5-recipe-generation#team-members">NLP Engineers</a> to train my two prodigy recipe creators: <strong>Chef Scheherazade</strong> and <strong>Chef Giovanni</strong>.
|
@@ -28,8 +30,7 @@ The NLP engineers helped guide the learning process so that the chefs could actu
|
|
28 |
I trained my chefs by asking them to generate a title, a list of ingredients (including amounts!), and a list of directions after giving them just a simple list of food items.
|
29 |
</p>
|
30 |
|
31 |
-
<pre>
|
32 |
-
[Input]
|
33 |
{food items*: separated by comma}
|
34 |
|
35 |
[Targets]
|
|
|
1 |
HEADER_INFO = """""".strip()
|
2 |
SIDEBAR_INFO = """
|
3 |
+
<div class="contributors font-body text-bold">
|
4 |
<a class="contributor comma" href="https://huggingface.co/m3hrdadfi">Mehrdad Farahani</a>
|
5 |
<a class="contributor comma" href="https://huggingface.co/dk-crazydiv">Kartik Godawat</a>
|
6 |
<a class="contributor comma" href="https://huggingface.co/hassiahk">Haswanth Aekula</a>
|
|
|
9 |
</div>
|
10 |
"""
|
11 |
CHEF_INFO = """
|
12 |
+
<h2 class="font-title">Welcome to our lovely restaurant! </h2>
|
13 |
+
<p class="strong font-body">
|
14 |
<span class="d-block extra-info">(We are at your service with two of the best chefs in the world: Chef Scheherazade,
|
15 |
+
Chef Giovanni. Scheherazade is known for being more creative whereas Giovanni is more meticulous.)</span>
|
16 |
+
</p>
|
17 |
""".strip()
|
18 |
PROMPT_BOX = "Add custom ingredients here (separated by `,`): "
|
19 |
STORY = """
|
20 |
+
<div class="story-box font-body">
|
21 |
<p>
|
22 |
Hello everyone 👋, I am <strong>Chef Transformer</strong>,
|
23 |
the owner of this restaurant. I was made by a group of <a href="https://huggingface.co/flax-community/t5-recipe-generation#team-members">NLP Engineers</a> to train my two prodigy recipe creators: <strong>Chef Scheherazade</strong> and <strong>Chef Giovanni</strong>.
|
|
|
30 |
I trained my chefs by asking them to generate a title, a list of ingredients (including amounts!), and a list of directions after giving them just a simple list of food items.
|
31 |
</p>
|
32 |
|
33 |
+
<pre>[Inputs]
|
|
|
34 |
{food items*: separated by comma}
|
35 |
|
36 |
[Targets]
|
Build Ingredients Vocab.ipynb → notes/Build Ingredients Vocab.ipynb
RENAMED
File without changes
|
utils/__init__.py
ADDED
File without changes
|
utils/api.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import random
|
2 |
+
import requests
|
3 |
+
|
4 |
+
|
5 |
+
def generate_cook_image(query, app_id, app_key):
|
6 |
+
api_url = f"https://api.edamam.com/api/recipes/v2?type=public&q={query}&app_id={app_id}&app_key={app_key}&field=image"
|
7 |
+
|
8 |
+
try:
|
9 |
+
r = requests.get(api_url)
|
10 |
+
if r.status_code != 200:
|
11 |
+
return None
|
12 |
+
|
13 |
+
rj = r.json()
|
14 |
+
if "hits" not in rj or not len(rj["hits"]) > 0:
|
15 |
+
return None
|
16 |
+
|
17 |
+
data = rj["hits"]
|
18 |
+
data = data[random.randint(1, min(5, len(data) - 1))] if len(data) > 1 else data[0]
|
19 |
+
|
20 |
+
if "recipe" not in data or "image" not in data["recipe"]:
|
21 |
+
return None
|
22 |
+
|
23 |
+
image = data["recipe"]["image"]
|
24 |
+
return image
|
25 |
+
except Exception as e:
|
26 |
+
return None
|
utils/draw.py
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from PIL import (
|
2 |
+
Image,
|
3 |
+
ImageFont,
|
4 |
+
ImageDraw
|
5 |
+
)
|
6 |
+
import textwrap
|
7 |
+
from .utils import load_image_from_url
|
8 |
+
from .ext import (
|
9 |
+
ingredients as ext_ingredients,
|
10 |
+
directions as ext_directions
|
11 |
+
)
|
12 |
+
|
13 |
+
|
14 |
+
def generate_food_with_logo_image(bg_path, logo_path, food_url, no_food="asset/frame/no_food.png"):
|
15 |
+
bg = Image.open(bg_path)
|
16 |
+
width, height = bg.size
|
17 |
+
|
18 |
+
logo = Image.open(logo_path)
|
19 |
+
logo_width, logo_height, logo_ratio, logo_rb, logo_mb = logo.size + (3, -20, 45)
|
20 |
+
logo_width, logo_height = (logo_width // logo_ratio, logo_height // logo_ratio)
|
21 |
+
logo = logo.resize((logo_width, logo_height))
|
22 |
+
|
23 |
+
food = load_image_from_url(food_url, rgba_mode=True, default_image=no_food)
|
24 |
+
|
25 |
+
food_width, food_height = (300, 300)
|
26 |
+
food = food.resize((food_width, food_height))
|
27 |
+
|
28 |
+
bg.paste(food, (0, 0), food)
|
29 |
+
bg.paste(logo, (width - logo_width - logo_rb, height - logo_height - logo_mb), logo)
|
30 |
+
|
31 |
+
return bg
|
32 |
+
|
33 |
+
|
34 |
+
def generate_recipe_image(
|
35 |
+
recipe_data,
|
36 |
+
bg_path,
|
37 |
+
food_logo_ia,
|
38 |
+
fonts,
|
39 |
+
bg_color="#ffffff"
|
40 |
+
):
|
41 |
+
bg = Image.open(bg_path)
|
42 |
+
bg.paste(food_logo_ia, (50, 50), food_logo_ia)
|
43 |
+
bg_color = Image.new("RGBA", bg.size, bg_color)
|
44 |
+
bg_color.paste(bg, mask=bg)
|
45 |
+
|
46 |
+
im_editable = ImageDraw.Draw(bg_color)
|
47 |
+
im_editable.text(
|
48 |
+
(418, 30),
|
49 |
+
textwrap.fill(recipe_data["title"], 15).replace(" \n", "\n"),
|
50 |
+
(61, 61, 70),
|
51 |
+
font=fonts["title"],
|
52 |
+
)
|
53 |
+
|
54 |
+
im_editable.text(
|
55 |
+
(100, 450),
|
56 |
+
"Ingredients",
|
57 |
+
(61, 61, 70),
|
58 |
+
font=fonts["body_bold"],
|
59 |
+
)
|
60 |
+
ingredients = recipe_data["ingredients"]
|
61 |
+
ingredients = [textwrap.fill(item, 30).replace("\n", "\n ") for item in ingredients]
|
62 |
+
ingredients = ext_ingredients(ingredients, [], without_mapping=True)
|
63 |
+
|
64 |
+
im_editable.text(
|
65 |
+
(50, 520),
|
66 |
+
"\n".join([f"- {item}" for item in ingredients]),
|
67 |
+
(61, 61, 70),
|
68 |
+
font=fonts["body"],
|
69 |
+
)
|
70 |
+
|
71 |
+
im_editable.text(
|
72 |
+
(700, 450),
|
73 |
+
"Directions",
|
74 |
+
(61, 61, 70),
|
75 |
+
font=fonts["body_bold"],
|
76 |
+
)
|
77 |
+
|
78 |
+
directions = recipe_data["directions"]
|
79 |
+
directions = [textwrap.fill(item, 70).replace("\n", "\n ").capitalize() for item in directions]
|
80 |
+
im_editable.text(
|
81 |
+
(430, 520),
|
82 |
+
"\n".join([f"{i + 1}. {item}" for i, item in enumerate(directions)]).strip(),
|
83 |
+
(61, 61, 70),
|
84 |
+
font=fonts["body"],
|
85 |
+
)
|
86 |
+
return bg_color
|
utils/ext.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import re
|
2 |
+
from .utils import replace_regex
|
3 |
+
|
4 |
+
DEFAULT_MAP_DICT = {
|
5 |
+
" c ": " c. ",
|
6 |
+
", chopped": " (chopped)",
|
7 |
+
", crumbled": " (crumbled)",
|
8 |
+
", thawed": " (thawed)",
|
9 |
+
", melted": " (melted)",
|
10 |
+
}
|
11 |
+
|
12 |
+
|
13 |
+
def ingredient(text, map_dict):
|
14 |
+
if len(map_dict) > 0:
|
15 |
+
map_dict.update(**DEFAULT_MAP_DICT)
|
16 |
+
else:
|
17 |
+
map_dict = DEFAULT_MAP_DICT
|
18 |
+
|
19 |
+
text = replace_regex(text, map_dict)
|
20 |
+
text = re.sub(r"(\d)\s(\d\/\d)", r" \1+\2 ", text)
|
21 |
+
text = " ".join([word.strip() for word in text.split() if word.strip()])
|
22 |
+
return text
|
23 |
+
|
24 |
+
|
25 |
+
def ingredients(text_list, item_list, without_mapping=False):
|
26 |
+
map_dict = {
|
27 |
+
item: f'<span class="text-bold">{item}</span>' for item in list(map(lambda x: x.lower().strip(), item_list))
|
28 |
+
}
|
29 |
+
text_list = list(map(lambda x: x.lower(), text_list))
|
30 |
+
|
31 |
+
output = []
|
32 |
+
for text in text_list:
|
33 |
+
map_dict = map_dict if not without_mapping else {}
|
34 |
+
text = ingredient(text, map_dict)
|
35 |
+
output.append(text)
|
36 |
+
|
37 |
+
return output
|
38 |
+
|
39 |
+
|
40 |
+
def directions(text_list):
|
41 |
+
text_list = list(map(lambda x: x.lower().capitalize(), text_list))
|
42 |
+
|
43 |
+
return text_list
|
utils/st.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
|
3 |
+
|
4 |
+
def local_css(css_path):
|
5 |
+
with open(css_path) as f:
|
6 |
+
st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
|
7 |
+
|
8 |
+
|
9 |
+
def remote_css(css_url):
|
10 |
+
st.markdown(f'<link href="{css_url}" rel="stylesheet">', unsafe_allow_html=True)
|
utils/utils.py
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import base64
|
2 |
+
import json
|
3 |
+
from io import BytesIO
|
4 |
+
from PIL import Image
|
5 |
+
import requests
|
6 |
+
import re
|
7 |
+
|
8 |
+
|
9 |
+
def load_image_from_local(image_path, image_resize=None):
|
10 |
+
image = Image.open(image_path)
|
11 |
+
|
12 |
+
if isinstance(image_resize, tuple):
|
13 |
+
image = image.resize(image_resize)
|
14 |
+
return image
|
15 |
+
|
16 |
+
|
17 |
+
def load_image_from_url(image_url, rgba_mode=False, image_resize=None, default_image=None):
|
18 |
+
try:
|
19 |
+
image = Image.open(requests.get(image_url, stream=True).raw)
|
20 |
+
|
21 |
+
if rgba_mode:
|
22 |
+
image = image.convert("RGBA")
|
23 |
+
|
24 |
+
if isinstance(image_resize, tuple):
|
25 |
+
image = image.resize(image_resize)
|
26 |
+
|
27 |
+
except Exception as e:
|
28 |
+
image = None
|
29 |
+
if default_image:
|
30 |
+
image = load_image_from_local(default_image, image_resize=image_resize)
|
31 |
+
|
32 |
+
return image
|
33 |
+
|
34 |
+
|
35 |
+
def load_text(text_path):
|
36 |
+
text = ''
|
37 |
+
with open(text_path) as f:
|
38 |
+
text = f.read()
|
39 |
+
|
40 |
+
return text
|
41 |
+
|
42 |
+
|
43 |
+
def load_json(json_path):
|
44 |
+
jdata = ''
|
45 |
+
with open(json_path) as f:
|
46 |
+
jdata = json.load(f)
|
47 |
+
|
48 |
+
return jdata
|
49 |
+
|
50 |
+
|
51 |
+
def image_to_base64(image_array):
|
52 |
+
buffered = BytesIO()
|
53 |
+
image_array.save(buffered, format="PNG")
|
54 |
+
image_b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
55 |
+
return f"data:image/png;base64, {image_b64}"
|
56 |
+
|
57 |
+
|
58 |
+
def unique_list(seq):
|
59 |
+
seen = set()
|
60 |
+
seen_add = seen.add
|
61 |
+
return [x for x in seq if not (x in seen or seen_add(x))]
|
62 |
+
|
63 |
+
|
64 |
+
def pure_comma_separation(list_str, return_list=True):
|
65 |
+
r = unique_list([item.strip() for item in list_str.lower().split(",") if item.strip()])
|
66 |
+
if return_list:
|
67 |
+
return r
|
68 |
+
return ", ".join(r)
|
69 |
+
|
70 |
+
|
71 |
+
def replace_regex(text, map_dict):
|
72 |
+
pattern = "|".join(map(re.escape, map_dict.keys()))
|
73 |
+
return re.sub(pattern, lambda m: map_dict[m.group()], str(text))
|