Skip to content

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 2 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions examples/recipegenerator/README.md
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:
Copy link
Contributor

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?

Copy link
Contributor

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


```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.
44 changes: 44 additions & 0 deletions examples/recipegenerator/app.py
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)
104 changes: 104 additions & 0 deletions examples/recipegenerator/index.js
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();



20 changes: 20 additions & 0 deletions examples/recipegenerator/package.json
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"
}
}

15 changes: 15 additions & 0 deletions examples/recipegenerator/recipegenerator.gpt
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
2 changes: 2 additions & 0 deletions examples/recipegenerator/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Flask==2.0.1
Werkzeug==2.2.2
102 changes: 102 additions & 0 deletions examples/recipegenerator/templates/index.html
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>
11 changes: 11 additions & 0 deletions examples/recipegenerator/tool.gpt
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}