-
Notifications
You must be signed in to change notification settings - Fork 300
Add Recipe Generator sample application to examples #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# Recipe Generator Application | ||
|
||
## Overview | ||
|
||
This Flask application leverages GPTScript and GPTScript Vision to suggest recipes based on an image uploaded by the user. By analyzing the image, the application can identify ingredients and propose a suitable recipe. | ||
|
||
## Features | ||
|
||
- Image upload functionality. | ||
- Automatic ingredient recognition from images. | ||
- Recipe suggestion based on identified ingredients. | ||
|
||
## Installation | ||
|
||
### Prerequisites | ||
|
||
- Python 3.8 or later | ||
- Node.js and npm | ||
- Flask | ||
- Other Python and Node.js dependencies listed in `requirements.txt` and `package.json` respectively. | ||
|
||
### Steps | ||
|
||
1. Clone the repository: | ||
|
||
``` bash | ||
git clone https://github.com/gptscript-ai/gptscript.git | ||
``` | ||
|
||
2. Navigate to the `examples/recipegenerator` directory and install the dependencies: | ||
|
||
Python: | ||
|
||
```bash | ||
pip install -r requirements.txt | ||
``` | ||
|
||
Node: | ||
|
||
```bash | ||
npm install | ||
``` | ||
|
||
3. Setup `OPENAI_API_KEY` (Eg: `export OPENAI_API_KEY="yourapikey123456"`). You can get your [API key here](https://platform.openai.com/api-keys). | ||
|
||
4. Run the Flask application using `flask run` or `python app.py` | ||
|
||
## Usage | ||
|
||
1. Open your web browser and navigate to `http://127.0.0.1:5000/`. | ||
2. Use the web interface to upload an image with some grocery items. | ||
3. The application will process the image, identify potential ingredients, and suggest a recipe based on the analysis. | ||
4. View the suggested recipe, try it and let us know how it turned out to be! | ||
|
||
## Under the hood | ||
|
||
Below are the processes that take place when you execute the application: | ||
|
||
- The Python app places the uploaded image as `grocery.png` in the current working directory. | ||
- It then executes `recipegenerator.gpt` which internally calls `tools.gpt` to perform image analysis to identify the items from the uploaded image. | ||
- The identified ingredients from the image will be stored in a `response.json` file. | ||
- The recipegenerator will then read this response file, generate a recipe and add it to a recipe.md file. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
from flask import Flask, request, render_template, jsonify | ||
import subprocess | ||
import os | ||
|
||
app = Flask(__name__) | ||
|
||
# Setting the base directory | ||
base_dir = os.path.dirname(os.path.abspath(__file__)) | ||
app.config['UPLOAD_FOLDER'] = base_dir | ||
|
||
SCRIPT_PATH = os.path.join(base_dir, 'recipegenerator.gpt') | ||
GROCERY_PHOTO_FILE_NAME = 'grocery.png' # The expected file name | ||
RECIPE_FILE_NAME = 'recipe.md' # The output file name | ||
|
||
@app.route('/', methods=['GET']) | ||
def index(): | ||
return render_template('index.html') | ||
|
||
@app.route('/upload', methods=['POST']) | ||
def upload_file(): | ||
if 'file' not in request.files: | ||
return jsonify({'error': 'No file part'}), 400 | ||
file = request.files['file'] | ||
if file.filename == '': | ||
return jsonify({'error': 'No selected file'}), 400 | ||
if file: | ||
filename = os.path.join(app.config['UPLOAD_FOLDER'], GROCERY_PHOTO_FILE_NAME) | ||
file.save(filename) | ||
try: | ||
# Execute the script to generate the recipe | ||
subprocess.Popen(f"gptscript {SCRIPT_PATH}", shell=True, stdout=subprocess.PIPE, cwd=base_dir).stdout.read() | ||
|
||
# Read recipe.md file | ||
recipe_file_path = os.path.join(app.config['UPLOAD_FOLDER'], RECIPE_FILE_NAME) | ||
with open(recipe_file_path, 'r') as recipe_file: | ||
recipe_content = recipe_file.read() | ||
|
||
# Return recipe content | ||
return jsonify({'recipe': recipe_content}) | ||
except Exception as e: | ||
return jsonify({'error': str(e)}), 500 | ||
|
||
if __name__ == '__main__': | ||
app.run(debug=False) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { Command, Option } from 'commander'; | ||
import { fileTypeFromBuffer } from 'file-type'; | ||
import { URL } from 'whatwg-url'; | ||
import fs from 'fs'; | ||
import OpenAI from 'openai'; | ||
|
||
|
||
async function main() { | ||
const program = new Command(); | ||
|
||
program.description('Utility for processing images with the OpenAI API'); | ||
|
||
program.addOption(new Option('--openai-api-key <key>', 'OpenAI API Key') | ||
.env('OPENAI_API_KEY') | ||
.makeOptionMandatory() | ||
); | ||
|
||
program.addOption(new Option('--openai-base-url <string>', 'OpenAI base URL') | ||
.env('OPENAI_BASE_URL') | ||
); | ||
|
||
program.addOption(new Option('--openai-org-id <string>', 'OpenAI Org ID to use') | ||
.env('OPENAI_ORG_ID') | ||
); | ||
|
||
program.addOption(new Option('--max-tokens <number>', 'Max tokens to use') | ||
.default(2048) | ||
.env('MAX_TOKENS') | ||
); | ||
|
||
program.addOption(new Option('--model <model>', 'Model to process images with') | ||
.env('MODEL') | ||
.choices(['gpt-4-vision-preview']) | ||
.default('gpt-4-vision-preview') | ||
); | ||
|
||
program.addOption(new Option('--detail <detail>', 'Fidelity to use when processing images') | ||
.env('DETAIL') | ||
.choices(['low', 'high', 'auto']) | ||
.default('auto') | ||
); | ||
|
||
program.argument('<prompt>', 'Prompt to send to the vision model'); | ||
|
||
program.argument('<images...>', 'List of image URIs to process. Supports file:// and https:// protocols. Images must be jpeg or png.'); | ||
|
||
program.action(run); | ||
await program.parseAsync(); | ||
} | ||
|
||
async function run(prompt, images, opts) { | ||
let content = [] | ||
for (let image of images) { | ||
content.push({ | ||
type: "image_url", | ||
image_url: { | ||
detail: opts.detail, | ||
url: await resolveImageURL(image) | ||
} | ||
}) | ||
} | ||
|
||
const openai = new OpenAI(opts.openaiApiKey, opts.baseUrl, opts.orgId); | ||
const response = await openai.chat.completions.create({ | ||
model: opts.model, | ||
max_tokens: opts.maxTokens, | ||
messages: [ | ||
{ | ||
role: 'user', | ||
content: [ | ||
{ type: "text", text: prompt }, | ||
...content | ||
] | ||
}, | ||
] | ||
}); | ||
|
||
console.log(JSON.stringify(response, null, 4)); | ||
} | ||
|
||
async function resolveImageURL(image) { | ||
const uri = new URL(image) | ||
switch (uri.protocol) { | ||
case 'http:': | ||
case 'https:': | ||
return image; | ||
case 'file:': | ||
const filePath = image.slice(7) | ||
const data = fs.readFileSync(filePath) | ||
const mime = (await fileTypeFromBuffer(data)).mime | ||
if (mime != 'image/jpeg' && mime != 'image/png') { | ||
throw new Error('Unsupported mimetype') | ||
} | ||
const base64 = data.toString('base64') | ||
return `data:${mime};base64,${base64}` | ||
default: | ||
throw new Error('Unsupported protocol') | ||
} | ||
} | ||
|
||
main(); | ||
|
||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"name": "vision", | ||
"version": "0.0.1", | ||
"description": "Utility for processing images with the OpenAI API", | ||
"exports": "./index.js", | ||
"type": "module", | ||
"scripts": { | ||
"start": "node index.js" | ||
}, | ||
"bin": "index.js", | ||
"keywords": [], | ||
"author": "", | ||
"dependencies": { | ||
"commander": "^9.0.0", | ||
"file-type": "^19.0.0", | ||
"openai": "^4.28.0", | ||
"whatwg-url": "^14.0.0" | ||
} | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
tools: sys.find, sys.read, sys.write, recipegenerator, tool.gpt | ||
|
||
Perform the following steps in order: | ||
|
||
1. Give me a list of 5 to 10 ingredients from the picture grocery.png located in the current directory and put those extracted ingredients into a list. | ||
2. Based on these ingredients list, suggest me one recipe that is quick to cook and create a new recipe.md file with the recipe | ||
|
||
|
||
--- | ||
name: recipegenerator | ||
description: Generate a recipe from the list of ingredients | ||
args: ingredients: a list of available ingredients. | ||
tools: sys.read | ||
|
||
Generate 1 new recipe based on the ingredients list |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Flask==2.0.1 | ||
techmaharaj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Werkzeug==2.2.2 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
<title>Recipe Generator</title> | ||
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous"> | ||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/showdown.min.js"></script> | ||
<link rel="stylesheet" href="styles.css"> | ||
<style> | ||
.loader { | ||
display: none; | ||
border: 4px solid #f3f3f3; | ||
border-top: 4px solid #3498db; | ||
border-radius: 50%; | ||
width: 30px; | ||
height: 30px; | ||
animation: spin 2s linear infinite; | ||
} | ||
@keyframes spin { | ||
0% { transform: rotate(0deg); } | ||
100% { transform: rotate(360deg); } | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> | ||
<div class="container"> | ||
<a class="navbar-brand" href="#">Recipe Generator</a> | ||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> | ||
<span class="navbar-toggler-icon"></span> | ||
</button> | ||
<div class="collapse navbar-collapse" id="navbarSupportedContent"> | ||
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> | ||
<li class="nav-item"> | ||
<a class="nav-link active" aria-current="page" href="#">Home</a> | ||
</li> | ||
<li class="nav-item"> | ||
<a class="nav-link" href="https://github.com/gptscript-ai/gptscript" target="_blank">GPTScript</a> | ||
</li> | ||
</ul> | ||
</div> | ||
</div> | ||
</nav> | ||
|
||
<div class="container my-5"> | ||
<h1>Recipe Generator</h1> | ||
<div class="col-lg-8 px-0"> | ||
<p class="fs-8">Don't know what to do with what's in your shopping cart? Well, click a picture and upload the image to Recipe Generator that will give you interesting ideas of what you can cook from those ingredients! This is built using <a href="https://github.com/gptscript-ai/gptscript" target="_blank">GPTScript</a>.</p> | ||
</div> | ||
</div> | ||
|
||
<div class="container my-5"> | ||
<div class="mb-3"> | ||
<form id="uploadForm"> | ||
<div class="input-group"> | ||
<input type="file" name="file" class="form-control" id="formFile" aria-describedby="inputGroupFileAddon04" aria-label="Upload"> | ||
<button class="btn btn-outline-secondary" type="button" id="inputGroupFileAddon04" onclick="uploadFile()">Upload File</button> | ||
</div> | ||
</form> | ||
</div> | ||
<div id="loader" class="loader"></div> | ||
<div id="recipeOutput"></div> | ||
<script> | ||
document.addEventListener('DOMContentLoaded', function() { | ||
// Define uploadFile globally | ||
window.uploadFile = function() { | ||
var form = document.getElementById('uploadForm'); | ||
var formData = new FormData(form); | ||
var loader = document.getElementById('loader'); | ||
var recipeOutput = document.getElementById('recipeOutput'); | ||
loader.style.display = 'block'; // Show the loader | ||
|
||
fetch('/upload', { | ||
method: 'POST', | ||
body: formData, | ||
}) | ||
.then(response => response.json()) // Parse the JSON response | ||
.then(data => { | ||
loader.style.display = 'none'; // Hide the loader | ||
if(data.recipe) { | ||
var converter = new showdown.Converter() | ||
var parsedHtml = converter.makeHtml(data.recipe); | ||
recipeOutput.innerHTML = parsedHtml; // Display the recipe | ||
} else if (data.error) { | ||
recipeOutput.innerHTML = `<p>Error: ${data.error}</p>`; | ||
} | ||
}) | ||
.catch(error => { | ||
console.error('Error:', error); | ||
loader.style.display = 'none'; | ||
recipeOutput.innerHTML = `<p>Error: ${error}</p>`; | ||
}); | ||
}; | ||
}); | ||
</script> | ||
</div> | ||
|
||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script> | ||
<script src="main.js"></script> | ||
</body> | ||
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
Name: vision | ||
Description: Analyze a set of images using a given prompt and vision model. | ||
Args: detail: Fidelity to process images at. One of ["high", "low", "auto"]. Default is "auto". | ||
Args: max-tokens: The maximum number of tokens. Default is 2048. | ||
Args: model: The name of the model to use. Default is "gpt-4-vision-preview". | ||
Args: prompt: The text prompt based on which the GPT model will generate a response. | ||
Args: images: Space-delimited list of image URIs to analyze. Valid URI protocols are "http" and "https" for remote images, and "file" for local images. Only supports jpeg and png. | ||
|
||
#!/bin/bash | ||
|
||
node index.js "${PROMPT}" ${IMAGES} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we include creating a virtualenv? or is that not necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
meh. let ppl decide that for themselves