visualref_docker / demo /app_client.py
bulatkh
Recsys demo based on VLMs + visual embeddings (#4)
5ae5072 unverified
Raw
History Blame Contribute Delete
19.8 kB
import argparse
import asyncio
import logging
from typing import List, Optional
import gradio as gr
from gradio_image_annotation import image_annotator
from PIL import Image
from client.retrieval_client import RemoteRetrievalClient
from utils.image_utils import base64_to_image, resize_images
from utils.utils import get_timestamp, load_yaml, save_json
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def parse_args():
parser = argparse.ArgumentParser(description="Retrieval Demo Client")
parser.add_argument(
"--config_path",
type=str,
required=True,
help="Path to the main configuration file"
)
parser.add_argument(
"--captioning_model_config_path",
type=str,
required=True,
help="Path to captioning model config file"
)
parser.add_argument(
"--server_url",
type=str,
default="http://localhost:8000",
help="URL of the retrieval server"
)
parser.add_argument(
"--timeout",
type=int,
default=60,
help="Request timeout in seconds"
)
parser.add_argument(
"--port",
type=int,
default=7860,
help="Port for the Gradio interface"
)
parser.add_argument(
"--share",
action="store_true",
help="Create a public link for the interface"
)
return parser.parse_args()
args = parse_args()
# Load configuration
config = load_yaml(args.config_path)
captioning_model_config = load_yaml(args.captioning_model_config_path)
logger.info(f"Loaded config from {args.config_path}")
# Initialize retrieval client
retrieval_client = RemoteRetrievalClient(server_url=args.server_url)
logger.info(f"Initialized remote client for {args.server_url}")
# Initialize logging
logs = {
"start_timestamp": get_timestamp(),
"config_path": args.config_path,
"captioning_model_config_path": args.captioning_model_config_path,
"server_url": args.server_url,
"experiments": {},
}
retrieval_round = 1
experiment_id = 0
# Store processed feedback embeddings
processed_feedback_embeddings = {
"positive_embeddings": None,
"negative_embeddings": None
}
# Functions calling the server: image search
async def image_search(search_query: str, top_k: int = 5):
"""Retrieve images based on text query"""
global retrieval_round, experiment_id
experiment_id += 1
logs["experiments"][experiment_id] = []
try:
logger.info(f"Searching for: {search_query}")
images, scores, retrieved_image_paths = await retrieval_client.search_images(search_query, top_k)
update_logs_retrieval(
experiment_id,
retrieval_round,
search_query,
top_k,
retrieved_image_paths,
scores
)
logger.info(f"Search completed successfully, found {len(images)} images")
return images, scores, retrieved_image_paths
except Exception as e:
logger.error(f"Search failed: {str(e)}")
return [], [], []
# Functions calling the server: process feedback
async def process_feedback(
feedback_query: str,
top_k: int,
image_paths: List[str],
annotator_boxes: List,
user_prompt: Optional[str] = None
):
"""Process feedback from the annotator and store embeddings for later application"""
global processed_feedback_embeddings
try:
logger.info(f"Processing feedback for query: {feedback_query}")
relevance_feedback_results = await retrieval_client.process_feedback(
query=feedback_query,
relevant_image_paths=image_paths,
annotator_json_boxes_list=annotator_boxes,
visualization=True,
top_k_feedback=top_k,
user_prompt=user_prompt,
prompt=captioning_model_config.get("PROMPT", None)
)
processed_feedback_embeddings["positive_embeddings"] = relevance_feedback_results.get("positive")
processed_feedback_embeddings["negative_embeddings"] = relevance_feedback_results.get("negative")
logger.info("Feedback processed successfully and embeddings stored")
return relevance_feedback_results
except Exception as e:
logger.error(f"Process feedback failed: {str(e)}")
return []
# Functions calling the server: apply feedback
async def apply_feedback(
feedback_query: str,
top_k: int,
relevant_captions: Optional[List[str]] = None,
irrelevant_captions: Optional[List[str]] = None,
fuse_query: bool = False,
use_stored_embeddings: bool = True
):
"""Apply feedback to the image search using stored processed embeddings"""
global retrieval_round, processed_feedback_embeddings
try:
logger.info(f"Applying feedback for query: {feedback_query}")
images, scores, retrieved_image_paths = await retrieval_client.apply_feedback(
query=feedback_query,
top_k=top_k,
relevant_captions=relevant_captions,
irrelevant_captions=irrelevant_captions,
fuse_initial_query=fuse_query
)
retrieval_round += 1
update_logs_retrieval(
experiment_id,
retrieval_round,
feedback_query,
top_k,
retrieved_image_paths,
scores
)
logger.info(f"Feedback applied successfully, found {len(images)} images")
return images, scores, retrieved_image_paths
except Exception as e:
logger.error(f"Apply feedback failed: {str(e)}")
return [], [], []
# Update logs after retrieval
def update_logs_retrieval(
experiment_id: int,
retrieval_round: int,
user_query: str,
top_k: int,
retrieved_image_paths: List[str],
scores: List[float],
):
"""Update logs with retrieval information"""
logs["experiments"][experiment_id].append({
"timestamp": get_timestamp(),
"type": "retrieval",
"round": retrieval_round,
"user_query": user_query,
"top_k": top_k,
"retrieved_image_paths": retrieved_image_paths,
"scores": scores,
})
try:
save_json(logs, config["RETRIEVAL_LOGS_PATH"])
except Exception as e:
logger.warning(f"Failed to save logs: {str(e)}")
# Update logs after feedback
def update_logs_feedback(
exp_id: int,
round_num: int,
user_query: str,
annotations: List,
relevant_features: Optional[str] = None,
irrelevant_features: Optional[str] = None
):
"""Update logs with feedback information"""
logs["experiments"][exp_id].append({
"timestamp": get_timestamp(),
"type": "feedback",
"round": round_num,
"user_query": user_query,
"annotations": annotations,
"relevant_textual_features": relevant_features.split(", ") if relevant_features else [],
"irrelevant_textual_features": irrelevant_features.split(", ") if irrelevant_features else [],
})
try:
save_json(logs, config["RETRIEVAL_LOGS_PATH"])
except Exception as e:
logger.warning(f"Failed to save logs: {str(e)}")
def get_boxes_json(annotations):
"""Get bounding boxes from annotator"""
return annotations["boxes"] if annotations["boxes"] else None
def format_outputs_image_search(images: List, scores: List[float], retrieved_image_paths: List[str]):
"""Format outputs for image search"""
outputs_annotators = []
outputs_gallery = []
outputs_retrieved_image_paths = []
outputs_images_with_saliency = None
images = resize_images(images, config)
for idx in range(len(images)):
outputs_annotators.append({"image": images[idx]})
outputs_gallery.append((images[idx], f"Relevance score: {scores[idx]:.4f}"))
outputs_retrieved_image_paths.append(retrieved_image_paths[idx])
final_outputs = [outputs_gallery] + [outputs_retrieved_image_paths] + [outputs_images_with_saliency] + outputs_annotators
return final_outputs
def format_outputs_process_feedback(
positive: List[float],
negative: List[float],
relevant_captions: str,
irrelevant_captions: str,
explanation: List[Image.Image]
):
"""Format outputs for process feedback"""
outputs_explanation = []
for idx in range(len(explanation)):
outputs_explanation.append(explanation[idx])
# Clean up captions
if relevant_captions:
if isinstance(relevant_captions, str):
relevant_captions_list = relevant_captions.split(", ")
else:
relevant_captions_list = relevant_captions
for idx, caption in enumerate(relevant_captions_list):
if caption.endswith("."):
relevant_captions_list[idx] = caption[:-1]
outputs_relevant_captions = ", ".join(relevant_captions_list)
else:
outputs_relevant_captions = ""
if irrelevant_captions:
if isinstance(irrelevant_captions, str):
irrelevant_captions_list = irrelevant_captions.split(", ")
else:
irrelevant_captions_list = irrelevant_captions
for idx, caption in enumerate(irrelevant_captions_list):
if caption.endswith("."):
irrelevant_captions_list[idx] = caption[:-1]
outputs_irrelevant_captions = ", ".join(irrelevant_captions_list)
else:
outputs_irrelevant_captions = ""
final_outputs = [outputs_relevant_captions] + [outputs_irrelevant_captions] + [outputs_explanation]
return final_outputs
def format_outputs_feedback(
images: List,
scores: List[float],
retrieved_image_paths: List[str],
images_with_saliency: List[Image.Image],
explanation: List[Image.Image]
):
"""Format outputs for feedback"""
outputs_annotators = []
outputs_gallery = []
outputs_retrieved_image_paths = []
images = resize_images(images, config)
for idx in range(len(images)):
outputs_annotators.append({"image": images[idx], "boxes": []})
outputs_gallery.append((images[idx], f"Relevance score: {scores[idx]:.4f}"))
outputs_retrieved_image_paths.append(retrieved_image_paths[idx])
final_outputs = [outputs_gallery] + [outputs_retrieved_image_paths] + outputs_annotators
return final_outputs
# Start of the Gradio interface
css = """
#warning {background-color: #FFCCCB}
.feedback {font-size: 20px !important;}
.feedback textarea {font-size: 20px !important;}
.server-status {background-color: #E8F5E8; padding: 10px; border-radius: 5px; margin: 10px 0;}
.error-message {background-color: #FFE6E6; padding: 10px; border-radius: 5px; margin: 10px 0;}
"""
with gr.Blocks(title="VisualReF: GenAI Captioning", css=css) as demo:
gr.Markdown("# Text-to-Image Search (GenAI Captioning)")
image_top_k = gr.State(value=config.get("TOP_K", 5))
fuse_initial_query = gr.State(value=config.get("FUSE_INITIAL_QUERY", True))
with gr.Tab("Image Search"):
with gr.Row():
with gr.Column():
query = gr.Textbox(
label="Describe the image you would like to find:",
placeholder="Enter your search query here..."
)
image_search_btn = gr.Button("Search Images", variant="primary")
error_display = gr.HTML(visible=False)
with gr.Row():
image_gallery = gr.Gallery(
label="Retrieved Images",
columns=5,
rows=1,
visible=config["SHOW_IMAGE_GALLERY"],
show_label=True
)
# Annotators for feedback
annotators = []
annotator_json_boxes_list = []
with gr.Row():
for i in range(image_top_k.value):
with gr.Column():
annotator = image_annotator(
value=None,
label_list=["Relevant", "Irrelevant"],
label_colors=[(0, 255, 0), (255, 0, 0)],
label=f"Result {i + 1}",
visible=config["SHOW_ANNOTATORS"],
sources=[],
)
annotators.append(annotator)
button_get = gr.Button(f"Get bounding boxes for Result {i + 1}")
annotator_json_boxes = gr.JSON(visible=True)
annotator_json_boxes_list.append(annotator_json_boxes)
button_get.click(get_boxes_json, inputs=annotator, outputs=annotator_json_boxes)
relevant_image_paths = gr.State(value=None)
with gr.Row():
user_prompt_text = gr.Textbox(
label="Instructions for the captioning model",
visible=True,
interactive=True,
placeholder="Enter instructions for the captioning model..."
)
with gr.Row():
process_feedback_btn = gr.Button("Process Feedback", variant="secondary")
with gr.Row():
feedback_explanation_gallery = gr.Gallery(
label="Feedback Explanations (Previous Round)",
columns=5,
rows=1,
visible=config["SHOW_IMAGE_GALLERY"]
)
async def handle_image_search(search_query, top_k):
try:
images, scores, retrieved_image_paths = await image_search(search_query, top_k)
formatted_outputs = format_outputs_image_search(images, scores, retrieved_image_paths)
return [gr.HTML(visible=False)] + formatted_outputs
except Exception as e:
logger.error(f"Image search error: {str(e)}")
image_search_btn.click(
fn=handle_image_search,
inputs=[query, image_top_k],
outputs=[error_display, image_gallery, relevant_image_paths, feedback_explanation_gallery, *annotators],
)
# Textual features input
with gr.Row():
with gr.Column():
relevant_features = gr.Textbox(
label="Relevant features",
visible=True,
interactive=True,
elem_classes=["feedback"],
placeholder="Enter relevant features separated by commas..."
)
with gr.Column():
irrelevant_features = gr.Textbox(
label="Irrelevant features",
visible=True,
interactive=True,
elem_classes=["feedback"],
placeholder="Enter irrelevant features separated by commas..."
)
# Process feedback button handler
async def handle_process_feedback(
feedback_query,
top_k,
image_paths,
user_prompt,
*annotator_boxes
):
try:
if not feedback_query.strip():
return ["", "", []]
logger.info(f"{feedback_query}, {top_k}, {image_paths}, {list(annotator_boxes)}")
relevance_feedback_results = await process_feedback(
feedback_query, top_k, image_paths, list(annotator_boxes), user_prompt)
explanation = relevance_feedback_results.get("explanation", [])
if explanation is not None:
explanation = [base64_to_image(img) for img in explanation]
relevance_feedback_results["explanation"] = explanation
logger.info(f"Relevance feedback results: {relevance_feedback_results}")
return format_outputs_process_feedback(
relevance_feedback_results.get("positive", []),
relevance_feedback_results.get("negative", []),
relevance_feedback_results.get("relevant_captions", ""),
relevance_feedback_results.get("irrelevant_captions", ""),
relevance_feedback_results.get("explanation", [])
)
except Exception as e:
logger.error(f"Process feedback error: {str(e)}")
return ["", "", []]
process_feedback_btn.click(
fn=handle_process_feedback,
inputs=[query, image_top_k, relevant_image_paths, user_prompt_text, *annotator_json_boxes_list],
outputs=[relevant_features, irrelevant_features, feedback_explanation_gallery],
)
# Apply feedback button
with gr.Row():
apply_feedback_btn = gr.Button("Apply Feedback", variant="primary")
# Apply feedback button handler
async def handle_apply_feedback(
feedback_query,
top_k,
relevant_captions,
irrelevant_captions,
fuse_query
):
try:
images, scores, retrieved_image_paths = await apply_feedback(
feedback_query,
top_k,
relevant_captions,
irrelevant_captions,
fuse_query
)
formatted_outputs = format_outputs_feedback(
images,
scores,
retrieved_image_paths,
[], # images_with_saliency - not used in current implementation
[] # explanation - not used in current implementation
)
# formatted_outputs = [gallery, retrieved_image_paths, *annotators]
# Expected outputs: [error_display, image_gallery, relevant_image_paths, *annotators]
gallery, retrieved_paths, *annotator_outputs = formatted_outputs
return [gr.HTML(visible=False), gallery, retrieved_paths] + annotator_outputs
except Exception as e:
logger.error(f"Apply feedback error: {str(e)}")
apply_feedback_btn.click(
fn=handle_apply_feedback,
inputs=[query, image_top_k, relevant_features, irrelevant_features, fuse_initial_query],
outputs=[error_display, image_gallery, relevant_image_paths, *annotators],
).then(
fn=lambda: [None for _ in annotator_json_boxes_list],
inputs=None,
outputs=[*annotator_json_boxes_list]
)
# Server health check
with gr.Tab("Server Status"):
gr.Markdown("## Server Information")
async def check_server_health():
try:
response = await retrieval_client.health()
return response
except Exception as e:
logger.error(f"Server health check error: {str(e)}")
return None
health_check_btn = gr.Button("Check Server Health")
health_display = gr.JSON()
health_check_btn.click(
fn=check_server_health,
outputs=[health_display]
)
# Cleanup function
async def cleanup():
"""Cleanup resources"""
try:
await retrieval_client.close()
logger.info("Client cleanup completed")
except Exception as e:
logger.error(f"Cleanup error: {str(e)}")
if __name__ == "__main__":
try:
logger.info(f"Starting client on port {args.port}")
demo.launch(
server_port=args.port,
share=args.share,
show_error=True
)
except KeyboardInterrupt:
logger.info("Shutting down client...")
except Exception as e:
logger.error(f"Client startup error: {str(e)}")
finally:
# Cleanup
asyncio.run(cleanup())