add image generation for placeholder images
This commit is contained in:
parent
3f071e7a68
commit
51c7334c0e
83
backend/image_generation.py
Normal file
83
backend/image_generation.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from openai import AsyncOpenAI
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
|
async def process_tasks(prompts):
|
||||||
|
tasks = [generate_image(prompt) for prompt in prompts]
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
processed_results = []
|
||||||
|
for result in results:
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
print(f"An exception occurred: {result}")
|
||||||
|
processed_results.append(None)
|
||||||
|
else:
|
||||||
|
processed_results.append(result)
|
||||||
|
|
||||||
|
return processed_results
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_image(prompt):
|
||||||
|
client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
|
||||||
|
image_params = {
|
||||||
|
"model": "dall-e-3",
|
||||||
|
"quality": "standard",
|
||||||
|
"style": "natural",
|
||||||
|
"n": 1,
|
||||||
|
"size": "1024x1024",
|
||||||
|
"prompt": prompt,
|
||||||
|
}
|
||||||
|
res = await client.images.generate(**image_params)
|
||||||
|
return res.data[0].url
|
||||||
|
|
||||||
|
|
||||||
|
def extract_dimensions(url):
|
||||||
|
# Regular expression to match numbers in the format '300x200'
|
||||||
|
matches = re.findall(r"(\d+)x(\d+)", url)
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
width, height = matches[0] # Extract the first match
|
||||||
|
width = int(width)
|
||||||
|
height = int(height)
|
||||||
|
return (width, height)
|
||||||
|
else:
|
||||||
|
return (100, 100)
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_images(code):
|
||||||
|
# Find all images and extract their alt texts
|
||||||
|
soup = BeautifulSoup(code, "html.parser")
|
||||||
|
images = soup.find_all("img")
|
||||||
|
alts = [img.get("alt", None) for img in images]
|
||||||
|
|
||||||
|
# Exclude images with no alt text
|
||||||
|
alts = [alt for alt in alts if alt is not None]
|
||||||
|
|
||||||
|
# Remove duplicates
|
||||||
|
prompts = list(set(alts))
|
||||||
|
|
||||||
|
# Generate images
|
||||||
|
results = await process_tasks(prompts)
|
||||||
|
|
||||||
|
# Create a dict mapping alt text to image URL
|
||||||
|
mapped_image_urls = dict(zip(prompts, results))
|
||||||
|
|
||||||
|
# Replace alt text with image URLs
|
||||||
|
for img in images:
|
||||||
|
new_url = mapped_image_urls[img.get("alt")]
|
||||||
|
|
||||||
|
if new_url:
|
||||||
|
# Set width and height attributes
|
||||||
|
width, height = extract_dimensions(img["src"])
|
||||||
|
img["width"] = width
|
||||||
|
img["height"] = height
|
||||||
|
# Replace img['src'] with the mapped image URL
|
||||||
|
img["src"] = new_url
|
||||||
|
else:
|
||||||
|
print("Image generation failed for alt text:" + img.get("alt"))
|
||||||
|
|
||||||
|
# Return the modified HTML
|
||||||
|
return str(soup)
|
||||||
@ -1,19 +1,26 @@
|
|||||||
# Load environment variables first
|
# Load environment variables first
|
||||||
import json
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from prompts import assemble_prompt
|
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
from fastapi import FastAPI, WebSocket
|
from fastapi import FastAPI, WebSocket
|
||||||
|
|
||||||
from llm import stream_openai_response
|
from llm import stream_openai_response
|
||||||
|
from mock import MOCK_HTML, mock_completion
|
||||||
|
from image_generation import generate_images
|
||||||
|
from prompts import assemble_prompt
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Useful for debugging purposes when you don't want to waste GPT4-Vision credits
|
||||||
|
# Setting to True will stream a mock response instead of calling the OpenAI API
|
||||||
|
SHOULD_MOCK_AI_RESPONSE = False
|
||||||
|
|
||||||
|
|
||||||
def write_logs(prompt_messages, completion):
|
def write_logs(prompt_messages, completion):
|
||||||
# Create run_logs directory if it doesn't exist
|
# Create run_logs directory if it doesn't exist
|
||||||
@ -41,6 +48,9 @@ async def stream_code_test(websocket: WebSocket):
|
|||||||
|
|
||||||
prompt_messages = assemble_prompt(params["image"])
|
prompt_messages = assemble_prompt(params["image"])
|
||||||
|
|
||||||
|
if SHOULD_MOCK_AI_RESPONSE:
|
||||||
|
completion = await mock_completion(process_chunk)
|
||||||
|
else:
|
||||||
completion = await stream_openai_response(
|
completion = await stream_openai_response(
|
||||||
prompt_messages,
|
prompt_messages,
|
||||||
lambda x: process_chunk(x),
|
lambda x: process_chunk(x),
|
||||||
@ -49,6 +59,20 @@ async def stream_code_test(websocket: WebSocket):
|
|||||||
# Write the messages dict into a log so that we can debug later
|
# Write the messages dict into a log so that we can debug later
|
||||||
write_logs(prompt_messages, completion)
|
write_logs(prompt_messages, completion)
|
||||||
|
|
||||||
await websocket.send_json({"type": "status", "value": "Code generation complete."})
|
# Generate images
|
||||||
|
await websocket.send_json({"type": "status", "value": "Generating images..."})
|
||||||
|
|
||||||
|
try:
|
||||||
|
updated_html = await generate_images(completion)
|
||||||
|
await websocket.send_json({"type": "setCode", "value": updated_html})
|
||||||
|
await websocket.send_json(
|
||||||
|
{"type": "status", "value": "Code generation complete."}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
print("Image generation failed", e)
|
||||||
|
await websocket.send_json(
|
||||||
|
{"type": "status", "value": "Image generation failed but code is complete."}
|
||||||
|
)
|
||||||
|
finally:
|
||||||
await websocket.close()
|
await websocket.close()
|
||||||
|
|||||||
140
backend/mock.py
Normal file
140
backend/mock.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
async def mock_completion(process_chunk):
|
||||||
|
code_to_return = MOCK_HTML_2
|
||||||
|
|
||||||
|
for i in range(0, len(code_to_return), 10):
|
||||||
|
await process_chunk(code_to_return[i : i + 10])
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
return code_to_return
|
||||||
|
|
||||||
|
|
||||||
|
MOCK_HTML = """<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Product Showcase</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-black text-white">
|
||||||
|
<nav class="py-6">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img src="https://placehold.co/24x24" alt="Company Logo" class="mr-8">
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">Store</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">Mac</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">iPad</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">iPhone</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">Watch</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">Vision</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">AirPods</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">TV & Home</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">Entertainment</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4">Accessories</a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium">Support</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a href="#" class="text-white text-sm font-medium mr-4"><i class="fas fa-search"></i></a>
|
||||||
|
<a href="#" class="text-white text-sm font-medium"><i class="fas fa-shopping-bag"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="mt-8">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<img src="https://placehold.co/100x100" alt="Brand Logo" class="mx-auto mb-4">
|
||||||
|
<h1 class="text-5xl font-bold mb-4">WATCH SERIES 9</h1>
|
||||||
|
<p class="text-2xl font-medium mb-8">Smarter. Brighter. Mightier.</p>
|
||||||
|
<div class="flex justify-center space-x-4">
|
||||||
|
<a href="#" class="text-blue-600 text-sm font-medium">Learn more ></a>
|
||||||
|
<a href="#" class="text-blue-600 text-sm font-medium">Buy ></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-12">
|
||||||
|
<img src="https://placehold.co/500x300" alt="Product image of a smartwatch with a pink band and a circular interface displaying various health metrics." class="mr-8">
|
||||||
|
<img src="https://placehold.co/500x300" alt="Product image of a smartwatch with a blue band and a square interface showing a classic analog clock face." class="ml-8">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
MOCK_HTML_2 = """
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>The New York Times - News</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Libre Franklin', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<header class="border-b border-gray-300 py-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<button class="text-gray-700"><i class="fas fa-bars"></i></button>
|
||||||
|
<button class="text-gray-700"><i class="fas fa-search"></i></button>
|
||||||
|
<div class="text-xs uppercase tracking-widest">Tuesday, November 14, 2023<br>Today's Paper</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img src="https://placehold.co/200x50?text=The+New+York+Times+Logo" alt="The New York Times Logo" class="h-8">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<button class="bg-black text-white px-4 py-1 text-xs uppercase tracking-widest">Give the times</button>
|
||||||
|
<div class="text-xs">Account</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="flex justify-between items-center py-4">
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<a href="#" class="text-xs uppercase tracking-widest text-gray-700">U.S.</a>
|
||||||
|
<!-- Add other navigation links as needed -->
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<a href="#" class="text-xs uppercase tracking-widest text-gray-700">Cooking</a>
|
||||||
|
<!-- Add other navigation links as needed -->
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section class="py-6">
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<article class="mb-4">
|
||||||
|
<h2 class="text-xl font-bold mb-2">Israeli Military Raids Gaza’s Largest Hospital</h2>
|
||||||
|
<p class="text-gray-700 mb-2">Israeli troops have entered the Al-Shifa Hospital complex, where conditions have grown dire and Israel says Hamas fighters are embedded.</p>
|
||||||
|
<a href="#" class="text-blue-600 text-sm">See more updates <i class="fas fa-external-link-alt"></i></a>
|
||||||
|
</article>
|
||||||
|
<!-- Repeat for each news item -->
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1">
|
||||||
|
<article class="mb-4">
|
||||||
|
<img src="https://placehold.co/300x200?text=News+Image" alt="Flares and plumes of smoke over the northern Gaza skyline on Tuesday." class="mb-2">
|
||||||
|
<h2 class="text-xl font-bold mb-2">From Elvis to Elopements, the Evolution of the Las Vegas Wedding</h2>
|
||||||
|
<p class="text-gray-700 mb-2">The glittering city that attracts thousands of couples seeking unconventional nuptials has grown beyond the drive-through wedding.</p>
|
||||||
|
<a href="#" class="text-blue-600 text-sm">8 MIN READ</a>
|
||||||
|
</article>
|
||||||
|
<!-- Repeat for each news item -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
33
backend/poetry.lock
generated
33
backend/poetry.lock
generated
@ -22,6 +22,25 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-
|
|||||||
test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||||
trio = ["trio (<0.22)"]
|
trio = ["trio (<0.22)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "beautifulsoup4"
|
||||||
|
version = "4.12.2"
|
||||||
|
description = "Screen-scraping library"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.0"
|
||||||
|
files = [
|
||||||
|
{file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"},
|
||||||
|
{file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
soupsieve = ">1.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
html5lib = ["html5lib"]
|
||||||
|
lxml = ["lxml"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2023.7.22"
|
version = "2023.7.22"
|
||||||
@ -284,6 +303,18 @@ files = [
|
|||||||
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
|
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "soupsieve"
|
||||||
|
version = "2.5"
|
||||||
|
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
|
||||||
|
{file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
@ -440,4 +471,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "37bf71ae4f77aaeda11cbb524e3999464fceb20706135680a4c77add8712a847"
|
content-hash = "5e4aa03dda279f66a9b3d30f7327109bcfd395795470d95f8c563897ce1bff84"
|
||||||
|
|||||||
@ -13,6 +13,7 @@ uvicorn = "^0.24.0.post1"
|
|||||||
websockets = "^12.0"
|
websockets = "^12.0"
|
||||||
openai = "^1.2.4"
|
openai = "^1.2.4"
|
||||||
python-dotenv = "^1.0.0"
|
python-dotenv = "^1.0.0"
|
||||||
|
beautifulsoup4 = "^4.12.2"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|||||||
@ -12,7 +12,7 @@ function App() {
|
|||||||
);
|
);
|
||||||
const [generatedCode, setGeneratedCode] = useState<string>("");
|
const [generatedCode, setGeneratedCode] = useState<string>("");
|
||||||
const [referenceImages, setReferenceImages] = useState<string[]>([]);
|
const [referenceImages, setReferenceImages] = useState<string[]>([]);
|
||||||
const [console, setConsole] = useState<string[]>([]);
|
const [executionConsole, setExecutionConsole] = useState<string[]>([]);
|
||||||
const [blobUrl, setBlobUrl] = useState("");
|
const [blobUrl, setBlobUrl] = useState("");
|
||||||
|
|
||||||
const createBlobUrl = () => {
|
const createBlobUrl = () => {
|
||||||
@ -29,8 +29,11 @@ function App() {
|
|||||||
function (token) {
|
function (token) {
|
||||||
setGeneratedCode((prev) => prev + token);
|
setGeneratedCode((prev) => prev + token);
|
||||||
},
|
},
|
||||||
|
function (code) {
|
||||||
|
setGeneratedCode(code);
|
||||||
|
},
|
||||||
function (line) {
|
function (line) {
|
||||||
setConsole((prev) => [...prev, line]);
|
setExecutionConsole((prev) => [...prev, line]);
|
||||||
},
|
},
|
||||||
function () {
|
function () {
|
||||||
setAppState("CODE_READY");
|
setAppState("CODE_READY");
|
||||||
@ -67,7 +70,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-400 px-4 py-2 rounded text-sm hidden">
|
<div className="bg-gray-400 px-4 py-2 rounded text-sm hidden">
|
||||||
<h2 className="text-lg mb-4 border-b border-gray-800">Console</h2>
|
<h2 className="text-lg mb-4 border-b border-gray-800">Console</h2>
|
||||||
{console.map((line, index) => (
|
{executionConsole.map((line, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="border-b border-gray-400 mb-2 text-gray-600 font-mono"
|
className="border-b border-gray-400 mb-2 text-gray-600 font-mono"
|
||||||
@ -82,7 +85,7 @@ function App() {
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
Generating...
|
{executionConsole.slice(-1)[0]}
|
||||||
</div>
|
</div>
|
||||||
<CodePreview code={generatedCode} />
|
<CodePreview code={generatedCode} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const ERROR_MESSAGE =
|
|||||||
export function generateCode(
|
export function generateCode(
|
||||||
imageUrl: string,
|
imageUrl: string,
|
||||||
onChange: (chunk: string) => void,
|
onChange: (chunk: string) => void,
|
||||||
|
onSetCode: (code: string) => void,
|
||||||
onStatusUpdate: (status: string) => void,
|
onStatusUpdate: (status: string) => void,
|
||||||
onComplete: () => void
|
onComplete: () => void
|
||||||
) {
|
) {
|
||||||
@ -29,6 +30,8 @@ export function generateCode(
|
|||||||
onChange(response.value);
|
onChange(response.value);
|
||||||
} else if (response.type === "status") {
|
} else if (response.type === "status") {
|
||||||
onStatusUpdate(response.value);
|
onStatusUpdate(response.value);
|
||||||
|
} else if (response.type === "setCode") {
|
||||||
|
onSetCode(response.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user