Alexandros Popov commited on
Commit
23afe01
·
1 Parent(s): b1d5d84

changed git histories.

Browse files
Files changed (7) hide show
  1. .gitignore +14 -0
  2. .python-version +1 -0
  3. main.py +328 -0
  4. pyproject.toml +20 -0
  5. utils.py +5 -0
  6. uv.lock +0 -0
  7. visual_search_agent.py +120 -0
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .env
12
+ *jpg
13
+ todo
14
+ *png
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.10
main.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import imageio
3
+ import replicate
4
+ import gradio as gr
5
+ from dotenv import load_dotenv
6
+ from langfuse import Langfuse, observe, get_client
7
+ import numpy as np
8
+ import openai
9
+ import json
10
+ from pydantic import BaseModel, conint
11
+ from utils import encode_image
12
+ import asyncio
13
+ import logfire
14
+ from agents import (
15
+ Agent,
16
+ function_tool,
17
+ Runner,
18
+ WebSearchTool,
19
+ )
20
+ import tempfile
21
+ from functools import partial, update_wrapper
22
+ from visual_search_agent import seach_google_for_images
23
+ from agents import Agent, ItemHelpers, Runner, TResponseInputItem, trace
24
+
25
+ load_dotenv()
26
+ class ImageEvaluation(BaseModel):
27
+ score: conint(ge=1, le=10)
28
+ feedback: str
29
+
30
+ class HistoricalGrounding(BaseModel):
31
+ general_description: str
32
+ building_architecture: str
33
+ roads: str
34
+ transportation: str
35
+ people: str
36
+ people_clothing: str
37
+
38
+ image_description = """
39
+ This image shows a stunning coastal view of a Mediterranean city, likely Nice on the French Riviera. The scene features:
40
+
41
+ A wide, curving bay with striking turquoise and deep blue waters transitioning beautifully from the shoreline outward.
42
+
43
+ A long pebble beach with scattered people relaxing, sunbathing, and strolling near the surf.
44
+
45
+ Palm-lined promenades running parallel to the shore, adding a tropical feel.
46
+
47
+ A wide coastal road with smooth curves following the bay, flanked by pedestrian and cycling lanes.
48
+
49
+ A cityscape in the background, with mid-rise buildings painted in warm tones (yellows, reds, and creams), stretching up into the hills.
50
+
51
+ Clear blue skies that suggest warm, sunny weather.
52
+
53
+ It’s a postcard-perfect scene capturing both the relaxing beach atmosphere and the vibrant urban energy of the French Riviera."""
54
+
55
+
56
+ historic_description = """
57
+ In the 2nd century AD, the crescent-shaped bay that today we know as the Baie des Anges would have looked remarkably similar in outline—its wide sweep of pebbly shore backed by clear, turquoise waters—yet almost entirely undeveloped by permanent seaside structures. Instead of hotels and promenades, the littoral zone would have been bordered by low cliffs and patches of maquis scrub, with fishermen’s shingle huts and simple wooden docks marking the only human presence along the shore.
58
+
59
+ Along the coast, the principal artery was the Via Julia Augusta, a paved Roman road laid out in the late 1st century BC to link Italia to Hispania. It hugged the contours of the bay, its broad stone slabs worn smooth by the passage of carts and marching legions. The road would have run just above the high-tide line, with occasional way-stations (mansiones) where travelers could rest and change horses ([fr.wikipedia.org](https://fr.wikipedia.org/wiki/Cemenelum?utm_source=chatgpt.com)).
60
+
61
+ Instead of the elegant, palm-lined Promenade des Anglais, one would have seen groves of olive and fig trees and stands of umbrella pines and cypresses—common Mediterranean species carefully tended for their oil, fruit, and timber. Beyond these, terraced plots of vineyards stretched partway up the hills, their produce carried down to small coastal landing places in flat-bottomed caiques (fishing boats) moored in shallow coves ([en.wikipedia.org](https://en.wikipedia.org/wiki/Nice?utm_source=chatgpt.com)).
62
+
63
+ Perched on the heights above the bay lay two distinct settlements. On the immediate seafront was Nikaia (Νίκαια), the older Greek foundation of c. 350 BC, which by the 2nd century AD had become a modest fishing and trading port, its stone quay dotted with amphorae-laden ships from Massalia (modern Marseille) or Sardinia. A small temple of Artemis likely crowned its acropolis, overlooking the masts and nets of local mariners ([en.wikipedia.org](https://en.wikipedia.org/wiki/Nice?utm_source=chatgpt.com)).
64
+
65
+ Further inland, on the hill now known as Cimiez, stood Cemenelum—the Roman civitas capital of the Alpes Maritimae province. Founded in 14 BC by Augustus as a military and administrative center, by the 2nd century it boasted a forum, elaborate public baths, and an amphitheater seating some 5,000 spectators ([intltravelnews.com](https://www.intltravelnews.com/2019/cemenelum-roman-city-french-riviera.html?utm_source=chatgpt.com), [bestofniceblog.com](https://www.bestofniceblog.com/what-to-see-in-nice/history-and-science-museums/roman-baths-ruins-cimiez-archeology-mueum/?utm_source=chatgpt.com)). From the beach one would see the white limestone walls of the bath complex rising among olive trees, steam curling from the caldarium’s hypocausts, while the rounded arches of the arena peered above nearby rooftops ([historytools.org](https://www.historytools.org/stories/journey-to-the-ancient-roman-heart-of-the-french-riviera-the-nice-cimiez-archaeological-museum?utm_source=chatgpt.com)).
66
+
67
+ Daily life on the beach would have contrasted sharply with today’s sunbathers and tourists. Instead, local Ligurian-Roman families in woolen tunics and leather sandals might gather at the water’s edge to wash wool or salt fish, while merchants in linen garments bartered amphorae of olive oil and imported wine on the shore. Slender fishermen’s boats plied the calm waters at dawn, their crews casting trammel nets into the blue-green depths.
68
+
69
+ Above all, the scene would have been one of a working Riviera: a blend of agricultural terraces, coastal trade, and provincial administration set within the timeless curve of sand and sea—far removed from the 21st-century bustle, yet already a crossroads of Mediterranean cultures under the Pax Romana.
70
+
71
+ """
72
+
73
+
74
+ logfire.configure(
75
+ service_name='my_agent_service',
76
+ send_to_logfire=False,
77
+ )
78
+ logfire.instrument_openai_agents()
79
+ langfuse = get_client()
80
+
81
+
82
+ twoBC_prompt = """Imagine how this image would look like in the 2nd BC."""
83
+
84
+ client = openai.OpenAI() # uses OPENAI_API_KEY
85
+
86
+ JUDGE_PROMPT = """
87
+ You are a historic critic.
88
+ You are provided with the description of scenes, a location and a year.
89
+ Your job is to judge how plausible the items describes belong that place at that era.
90
+
91
+ You must penalize items that are out-of-time.
92
+ Do not appraise the framing, the camera position or the camera technology.
93
+
94
+ You must rate this "truthfullness" on a scale of 1 to 10
95
+ and pricesely point out items that are out of time.
96
+ """
97
+
98
+ @observe(name="image_captionning", capture_input=False, as_type="generation")
99
+ def image_caption(image_path):
100
+
101
+ response = client.responses.create(
102
+ model="o4-mini-2025-04-16",
103
+ input=[{
104
+ "role": "user",
105
+ "content": [
106
+ {"type": "input_text", "text": "Describe this image, focusing on the human-maid items in the picture: buildings, roads, cloths,..."},
107
+ {
108
+ "type": "input_image",
109
+ "image_url": f"data:image/jpeg;base64,{encode_image(image_path)}",
110
+ },
111
+ ],
112
+ }],
113
+ )
114
+ return response.output_text
115
+
116
+
117
+
118
+
119
+ @observe(name="llm_judge", capture_input=False, as_type="generation") # creates a span; captures inputs/outputs automatically
120
+ def judge_answer(image_description, location, year):
121
+
122
+ response = client.responses.parse(
123
+ model="gpt-4o-mini",
124
+ input=[
125
+ {
126
+ "role": "system",
127
+ "content": [
128
+ {"type": "input_text", "text": JUDGE_PROMPT},
129
+ ],
130
+ },
131
+ {
132
+ "role": "user",
133
+ "content": [
134
+ {"type": "input_text", "text": f" image description : {image_description} . location : {location} . year : {year}"}
135
+ ],
136
+ }],
137
+ text_format=ImageEvaluation
138
+ )
139
+ return json.loads(response.output_text)
140
+
141
+
142
+ @observe(name="image-generation", as_type="generation")
143
+ def generate_image(picture_design, input_image, working_directory):
144
+ """
145
+ Calls the Replicate API to generate an image based on the input image.
146
+ Args:
147
+ prompt (str): The text prompt.
148
+ Returns:
149
+ str: Path to the generated image.
150
+ """
151
+ # Gradio provides the image as a numpy array, but the replicate library expects a file path
152
+ # So we save the numpy array as a temporary image file
153
+
154
+ prompt = f"""
155
+ You are an expert photoshop user.
156
+ You are given a photo and you must transform it as to what it would have looked like at a certain time period.
157
+
158
+ You must apply the changes described in: {picture_design}
159
+ """
160
+
161
+ if isinstance(input_image, np.ndarray):
162
+ temp_image_path = "temp_input_image.png"
163
+ imageio.imwrite(temp_image_path, input_image)
164
+ input_image = temp_image_path
165
+
166
+
167
+ with open(input_image, "rb") as image_file:
168
+ output = replicate.run(
169
+ "black-forest-labs/flux-kontext-pro",
170
+ input={
171
+ "prompt": prompt,
172
+ "input_image": image_file,
173
+ "aspect_ratio": "match_input_image",
174
+ "output_format": "jpg",
175
+ "safety_tolerance": 2,
176
+ "prompt_upsampling": False
177
+ }
178
+ )
179
+ num_images = len(os.listdir(working_directory))
180
+ output_image_path = os.path.join(working_directory, f"output_{num_images}.jpg")
181
+ print(f"Writing image in {output_image_path}")
182
+ with open(output_image_path, "wb") as f:
183
+ for chunk in output:
184
+ f.write(chunk)
185
+
186
+ return output_image_path
187
+
188
+ def create_rewind(image, text, date):
189
+ """
190
+ Processes the inputs from Gradio and generates an image and text.
191
+ """
192
+ prompt = f"{text} The scene is captured in the year {date}."
193
+ generated_image_path = generate_image(prompt)
194
+
195
+ output_text = f"This is the scene as it might have appeared in the year {date}."
196
+
197
+ return generated_image_path, output_text
198
+
199
+ @observe(name="historical_grounding", as_type="generation")
200
+ def get_historical_grounding(image_description, location, year):
201
+ instructions=f"""You are a historian. You are given the description of an image, a location and a time period.
202
+ You must reflect on what the scenary would look like at the period.
203
+ The nature of the scenary must remain unchanged : a seaside scenary remains a seaside scenary, a town center must remain a town center.
204
+ Be as historical accurate as possible about the items present in the image. Use the tools provided to search for images and look up information on internet.
205
+
206
+ For the visual description of the item, you can use the tools you are provided as well.
207
+ """
208
+ response = client.responses.parse(
209
+ model="gpt-5-mini-2025-08-07",
210
+ input=[
211
+ {"role": "system", "content": instructions},
212
+ {
213
+ "role": "user",
214
+ "content": f"image description: {image_description}, location: {location}, year: {year}",
215
+ },
216
+ ],
217
+ text_format=HistoricalGrounding,
218
+ tools=[{"type": "web_search_preview"}],
219
+ )
220
+ return response.output_text
221
+
222
+ def define_visual_cues_agent():
223
+ visual_cues_agent = Agent(
224
+ name="Visual Search Agent",
225
+ instructions="""You search for visual cues to illustrate specific items within a specific time period.
226
+ You provide a precise description of those items.
227
+ You can use the tools provided to search for images and look up information on internet et specific images.""",
228
+ model="gpt-5-mini-2025-08-07",
229
+ tools=[seach_google_for_images, WebSearchTool()],
230
+ )
231
+ return visual_cues_agent
232
+
233
+
234
+ def define_picture_designer_agent(image_path, working_directory):
235
+ picture_designer_agent = Agent(
236
+ name="Picture designer agent",
237
+ instructions="""
238
+ You are "picture designer" : you produce a text that will be used by an image generation tool
239
+ You receive as input the description of an image and a historical grounding of what that scene would look like at certain period of time.
240
+ Your goal is to modify the items visible on the image in such a way, that it would plausible that this image has been taken at period of time.
241
+
242
+ For instance, if the image is a picture of the Eiffel tower in Paris and the period is 3th BC, obviously the eiffel tower should be replaced by something else.
243
+
244
+ To help you in your task, you have access to the historical visual cue helper : this tool will provide you with precise descriptions of specific items, so that you can pricesely describe what to generate.
245
+
246
+ This text is to be interpreted by the image generation tool Replicate.
247
+ Some items in the description might be flagged as violent (butcher's clever), sexual (prostitutes), unsanitary (wastes)
248
+ Eliminate those possibly non-compliant items with the Replicate policy.
249
+ """,
250
+ model="o4-mini-2025-04-16",
251
+ tools=[WebSearchTool()],
252
+ handoffs=[define_visual_cues_agent()],
253
+ )
254
+ return picture_designer_agent
255
+
256
+
257
+ async def main(image_path, location, year) -> None:
258
+
259
+ working_directory = tempfile.mkdtemp("_scene_rewind")
260
+ print(f"Working in {working_directory}")
261
+
262
+ with trace("LLM as a judge"):
263
+ picture_description = image_caption(image_path)
264
+ historical_grounding = get_historical_grounding(picture_description, location, year)
265
+ picture_designer_agent = define_picture_designer_agent(image_path, working_directory)
266
+
267
+
268
+ input_items: list[TResponseInputItem] = [
269
+ {
270
+ "content": f"Description:{picture_description}\n Historical grounding: {historical_grounding}",
271
+ "role": "user"
272
+ }]
273
+ latest_outline: str | None = None
274
+
275
+
276
+ while True:
277
+ picture_design = await Runner.run(
278
+ picture_designer_agent,
279
+ input_items,
280
+ )
281
+
282
+ input_items = picture_design.to_input_list()
283
+ latest_outline = ItemHelpers.text_message_outputs(picture_design.new_items)
284
+ print("Story outline generated")
285
+
286
+ try:
287
+ output_path = generate_image(latest_outline, image_path, working_directory)
288
+ created_image_caption = image_caption(output_path)
289
+ judgment = judge_answer(created_image_caption, location, year)
290
+ except Exception:
291
+ judgment = {
292
+ "score": 0,
293
+ "feedback": "The image could not be produced as the content of the prompt as been flagged as sensitive by Replicate"
294
+ }
295
+
296
+ print(f"Evaluator score: {judgment['score']}")
297
+
298
+ if judgment["score"] > 6:
299
+ print("Story outline is good enough, exiting.")
300
+ break
301
+
302
+ print("Re-running with feedback")
303
+
304
+ input_items.append({"content": f"Feedback: {judgment['feedback']}", "role": "user"})
305
+
306
+ print(f"Final story outline: {latest_outline}")
307
+
308
+
309
+
310
+ if __name__ == "__main__":
311
+ image_path = "images/paris.png"
312
+ asyncio.run(main(image_path, "Paris", 1700))
313
+
314
+ # iface = gr.Interface(
315
+ # fn=create_rewind,
316
+ # inputs=[
317
+ # gr.Image(type="numpy", label="Input Image"),
318
+ # gr.Textbox(label="Prompt"),
319
+ # gr.Slider(minimum=-2000, maximum=2000, value=1900, label="Year")
320
+ # ],
321
+ # outputs=[
322
+ # gr.Image(type="filepath", label="Generated Image"),
323
+ # gr.Textbox(label="Generated Text")
324
+ # ],
325
+ # title="Scene Rewind",
326
+ # description="Upload an image, provide a prompt, and select a year to travel back in time!"
327
+ # )
328
+ # iface.launch()
pyproject.toml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "scene-rewind"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "dotenv>=0.9.9",
9
+ "google-search-results>=2.4.2",
10
+ "gradio>=5.42.0",
11
+ "imageio>=2.37.0",
12
+ "langfuse>=3.2.3",
13
+ "nest-asyncio>=1.6.0",
14
+ "openai>=1.99.6",
15
+ "openai-agents>=0.2.5",
16
+ "pydantic>=2.11.7",
17
+ "pydantic-ai[logfire]>=0.6.2",
18
+ "replicate>=1.0.7",
19
+ "serpapi>=0.1.5",
20
+ ]
utils.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import base64
2
+
3
+ def encode_image(image_path):
4
+ with open(image_path, "rb") as image_file:
5
+ return base64.b64encode(image_file.read()).decode("utf-8")
uv.lock ADDED
The diff for this file is too large to render. See raw diff
 
visual_search_agent.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from serpapi.google_search import GoogleSearch
2
+ from dotenv import load_dotenv
3
+ import os
4
+ from openai import OpenAI
5
+ import asyncio
6
+ import requests
7
+
8
+ from agents import enable_verbose_stdout_logging
9
+ enable_verbose_stdout_logging()
10
+
11
+ from agents import (
12
+ Agent,
13
+ function_tool,
14
+ Runner,
15
+ WebSearchTool,
16
+ )
17
+
18
+ load_dotenv()
19
+ SERP_API_KEY = os.getenv("SERP_API_KEY")
20
+ openai_client = OpenAI()
21
+
22
+ def describe_all_images(images_results):
23
+ descriptions = []
24
+ for image in images_results:
25
+ description = describe_thumbnail(image["thumbnail"])
26
+ descriptions.append({
27
+ "title": image["title"],
28
+ "link": image["link"],
29
+ "description": description
30
+ })
31
+ break
32
+ return descriptions
33
+
34
+ def describe_thumbnail(image_url):
35
+ response = openai_client.responses.create(
36
+ model="gpt-4.1",
37
+ input=[{
38
+ "role": "user",
39
+ "content": [
40
+ {"type": "input_text", "text": "what's in this image? To what era/year does it belong?"},
41
+ {
42
+ "type": "input_image",
43
+ "image_url": image_url,
44
+ },
45
+ ],
46
+ }],
47
+ )
48
+ return response.output_text + "\n"
49
+
50
+ def search_google(item_to_find: str):
51
+ params = {
52
+ "engine": "google_images_light",
53
+ "q": item_to_find,
54
+ "api_key": SERP_API_KEY
55
+ }
56
+
57
+ search = GoogleSearch(params)
58
+ raw_results = search.get_dict()["images_results"]
59
+ images_results = [
60
+ {
61
+ "thumbnail": result["thumbnail"],
62
+ "link": result["link"],
63
+ "title": result["title"]
64
+ }
65
+ for result in raw_results
66
+ ]
67
+ return raw_results
68
+
69
+ @function_tool
70
+ def seach_google_for_images(item_to_find: str) -> list:
71
+ """
72
+ Search Google Images for a given item and return a list of image results, with links and descriptions.
73
+
74
+ Args:
75
+ item_to_find (str): The item or query to search for in Google Images.
76
+
77
+ Returns:
78
+ list: A list of dictionaries, each containing the image title, link, and a description generated by the image analysis.
79
+ """
80
+ raw_results = search_google(item_to_find)
81
+ results_w_description = describe_all_images(raw_results)
82
+ return results_w_description
83
+
84
+ @function_tool
85
+ def wikipedia_lookup(query):
86
+ """
87
+ Look up a query on Wikipedia and return the summary extract of the most relevant page.
88
+
89
+ Args:
90
+ query (str): The search term to look up on Wikipedia.
91
+
92
+ Returns:
93
+ str: The summary extract of the most relevant Wikipedia page, or a message if no page is found.
94
+ """
95
+ # Step 1: Search
96
+ search_url = "https://en.wikipedia.org/w/rest.php/v1/search/title"
97
+ params = {"q": query, "limit": 1}
98
+ search_resp = requests.get(search_url, params=params)
99
+ search_resp.raise_for_status()
100
+ search_data = search_resp.json()
101
+
102
+ if not search_data.get("pages"):
103
+ return f"No Wikipedia page found for '{query}'"
104
+
105
+ page_key = search_data["pages"][0]["key"]
106
+
107
+ # Step 2: Fetch summary
108
+ summary_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{page_key}"
109
+ summary_resp = requests.get(summary_url)
110
+ summary_resp.raise_for_status()
111
+ return summary_resp.json().get("extract")
112
+
113
+
114
+
115
+ # if __name__ == "__main__":
116
+ # item_to_find = "mediterranean houses in the 16h century"
117
+ # results = search_google(item_to_find)
118
+ # desc = describe_all_images(results)
119
+ # print(desc[0])
120
+ # # print(wikipedia_lookup("United States Constitution"))