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 [email protected]:<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))
|