alexandraroze commited on
Commit
f11a85d
·
1 Parent(s): 92a2adc

dockerfile

Browse files
Files changed (8) hide show
  1. Dockerfile +25 -0
  2. app.py +5 -0
  3. poetry.lock +0 -0
  4. pyproject.toml +29 -0
  5. src/RAG.py +173 -0
  6. src/model.py +55 -0
  7. src/pipelines.py +149 -0
  8. src/prompts.py +72 -0
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ # Create a non-root user
4
+ RUN useradd -m -u 1000 user
5
+ USER user
6
+ ENV PATH="/home/user/.local/bin:$PATH"
7
+
8
+ # Set the working directory
9
+ WORKDIR /app
10
+
11
+ # Copy Poetry config and install Poetry
12
+ COPY --chown=user ./pyproject.toml ./poetry.lock* ./
13
+ RUN pip install --no-cache-dir --upgrade pip \
14
+ && pip install --no-cache-dir poetry \
15
+ && poetry config virtualenvs.create false \
16
+ && poetry install --no-dev --no-interaction --no-ansi
17
+
18
+ # Copy the rest of the app
19
+ COPY --chown=user . /app
20
+
21
+ # Expose the Streamlit default port
22
+ EXPOSE 8501
23
+
24
+ # Run the Streamlit app
25
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
app.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # streamlit example
2
+
3
+ import streamlit as st
4
+
5
+ st.title('Hello World!')
poetry.lock ADDED
The diff for this file is too large to render. See raw diff
 
pyproject.toml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "demo-pixtral-qwen"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = ["Your Name <[email protected]>"]
6
+
7
+ [tool.poetry.dependencies]
8
+ python = "^3.10"
9
+ clip = {git = "https://github.com/openai/CLIP.git", rev = "main"}
10
+ torchvision = "0.19"
11
+ vllm = "^0.6.3"
12
+ pillow = "10.3.0"
13
+ PyMuPDF = "^1.24.13"
14
+ pandas = "^2.2.3"
15
+ faiss-gpu = "^1.7.2"
16
+ huggingface = "^0.0.1"
17
+ huggingface-hub = "^0.26.2"
18
+ md2pdf = "^1.0.1"
19
+ pypdf = "^5.1.0"
20
+ wheel = "^0.45.1"
21
+ rerankers = {extras = ["all"], version = "^0.6.0"}
22
+ streamlit = "^1.40.1"
23
+ fuzzywuzzy = "^0.18.0"
24
+
25
+ [tool.poetry.dev-dependencies]
26
+
27
+ [build-system]
28
+ requires = ["poetry-core>=1.0.0"]
29
+ build-backend = "poetry.core.masonry.api"
src/RAG.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import clip
2
+ import faiss
3
+ from PIL import Image
4
+ from pypdf import PdfReader
5
+ import pandas as pd
6
+ import re
7
+ import os
8
+ import fitz
9
+ import torch
10
+ import numpy as np
11
+ from tqdm import tqdm
12
+ import base64
13
+
14
+
15
+ class RAG:
16
+ def __init__(
17
+ self,
18
+ fais_index_path,
19
+ clip_model="ViT-B/32",
20
+ reranker=None,
21
+ device="cpu",
22
+ image_invoice_index_path=None,
23
+ path_to_invoices=None,
24
+ path_to_images=None
25
+ ):
26
+ self.index = faiss.read_index(fais_index_path)
27
+ self.model, self.preprocess = clip.load(clip_model, device=device)
28
+ self.device = device
29
+ if image_invoice_index_path:
30
+ self.image_invoice_index = pd.read_csv(image_invoice_index_path)
31
+ self.path_to_invoices = path_to_invoices
32
+ self.path_to_images = path_to_images
33
+ self.reranker = reranker
34
+
35
+ @staticmethod
36
+ def image_to_base64(image_path):
37
+ with open(image_path, "rb") as image_file:
38
+ return base64.b64encode(image_file.read())
39
+
40
+ def search_text(self, text, k=1):
41
+ text_features = self.model.encode_text(clip.tokenize([text]).to(self.device))
42
+ text_features /= text_features.norm(dim=-1, keepdim=True)
43
+ text_features = text_features.detach().numpy()
44
+ distances, indices = self.index.search(text_features, k)
45
+ return distances, indices
46
+
47
+ def search_image(self, image=None, image_path=None, k=1):
48
+ if image is None and image_path is None:
49
+ raise ValueError("Either image or image_path must be provided.")
50
+ if image is None:
51
+ image = Image.open(image_path)
52
+ image_input = self.preprocess(image).unsqueeze(0).to(self.device)
53
+ image_features = self.model.encode_image(image_input)
54
+ image_features /= image_features.norm(dim=-1, keepdim=True)
55
+ image_features = image_features.detach().numpy()
56
+ distances, indices = self.index.search(image_features, k)
57
+ return distances, indices
58
+
59
+ def find_invoice(self, image=None, image_path=None, return_only_path=True, k=1, damage_description=None):
60
+ if self.image_invoice_index is None:
61
+ raise ValueError("No index for invoices found.")
62
+ _, indices = self.search_image(image=image, image_path=image_path, k=k)
63
+ img_ids = self.image_invoice_index.iloc[indices[0]]['img_id'].values
64
+ invoices = self.image_invoice_index[self.image_invoice_index['img_id'].isin(img_ids)]['invoice'].values.tolist()
65
+ images_paths = self.image_invoice_index[self.image_invoice_index['img_id'].isin(img_ids)]['image'].values.tolist()
66
+
67
+ if self.reranker:
68
+ if damage_description is None:
69
+ raise ValueError("Damage description must be provided.")
70
+ # images = [self.image_to_base64(f"{self.path_to_images}/{img_path}") for img_path in images_paths]
71
+
72
+ images = [f"{self.path_to_images}/{img_path}" for img_path in images_paths]
73
+ results = self.reranker.rank(damage_description, images, doc_ids=invoices)
74
+ invoices = [doc.doc_id for doc in results]
75
+ print(invoices)
76
+
77
+ if return_only_path:
78
+ return invoices, images_paths
79
+
80
+ if not self.path_to_invoices:
81
+ raise ValueError("Path to data must be provided.")
82
+
83
+ invoices_tables = []
84
+
85
+ for invoice in invoices:
86
+ pdf_path = f"{self.path_to_invoices}/{invoice}"
87
+ reader = PdfReader(pdf_path)
88
+ page = reader.pages[0]
89
+ text = page.extract_text()
90
+
91
+ table_text = re.search(r"Beschädigtes Teil.*?Gesamtsumme:.*?EUR", text, re.DOTALL).group()
92
+
93
+ lines = table_text.splitlines()
94
+ header = lines[0]
95
+ other_text = "\n".join(lines[1:])
96
+ cleaned_text = re.sub(r"(?<!\d)\n", " ", other_text)
97
+
98
+ table = header + "\n" + cleaned_text
99
+
100
+ inv = table.split("\n")
101
+ reformatted_inv = "Beschädigtes Teil | Teilkosten (EUR) | Arbeitsstunden | Arbeitskosten (EUR/Stunde) | Gesamtkosten (EUR)\n" + "\n".join(
102
+ " ".join(inv[i].split(" ")[:-4]) + " | " + ' | '.join(inv[i].split(" ")[-4:]) for i in
103
+ range(1, len(inv) - 1)) + "\n" + inv[-1]
104
+
105
+ invoices_tables.append(reformatted_inv)
106
+
107
+ return invoices_tables, invoices
108
+
109
+
110
+ def build_rag(directory):
111
+ invoices = os.listdir(f"{directory}/invoices_validated")
112
+ invoices = [i for i in invoices if i.endswith(".pdf")]
113
+
114
+ image_invoice = []
115
+ os.makedirs(f"{directory}/images", exist_ok=True)
116
+ os.makedirs(f"{directory}/invoices", exist_ok=True)
117
+
118
+ for invoice in invoices:
119
+ doc = fitz.open(f"{directory}/invoices_validated/{invoice}")
120
+
121
+ page = doc[1]
122
+ image_list = page.get_images(full=True)
123
+ text = page.get_text()
124
+
125
+ xref = image_list[0][0]
126
+ base_image = doc.extract_image(xref)
127
+ image_bytes = base_image["image"]
128
+ image_name = invoice.replace(".pdf", ".png")
129
+ with open(f"{directory}/images/{image_name}", "wb") as img_file:
130
+ img_file.write(image_bytes)
131
+
132
+ doc.delete_pages(range(1, doc.page_count))
133
+ doc.save(f"{directory}/invoices/{invoice}")
134
+ doc.close()
135
+
136
+ image_invoice.append({
137
+ "invoice": invoice,
138
+ "image": image_name,
139
+ "description": text
140
+ })
141
+
142
+ image_invoice = pd.DataFrame(image_invoice)
143
+
144
+ device = "cuda" if torch.cuda.is_available() else "cpu"
145
+ model, preprocess = clip.load("ViT-B/32", device=device)
146
+ images = image_invoice["image"].tolist()
147
+
148
+ embeddings = []
149
+ image_indices = []
150
+ img_ids = []
151
+
152
+ for idx, img_path in enumerate(tqdm(images)):
153
+ image = Image.open(f"{directory}/images/{img_path}")
154
+ img_ids.append(idx)
155
+ inputs = preprocess(image).unsqueeze(0).to(device)
156
+
157
+ with torch.no_grad():
158
+ image_embedding = model.encode_image(inputs)
159
+
160
+ image_embedding = image_embedding / image_embedding.norm(dim=-1, keepdim=True)
161
+ embeddings.append(image_embedding.cpu().numpy().astype("float32"))
162
+ image_indices.append(img_path)
163
+
164
+ image_invoice["img_id"] = img_ids
165
+ image_invoice.to_csv(f"{directory}/image_invoice.csv", index=False)
166
+
167
+ embeddings_np = np.vstack(embeddings)
168
+
169
+ dimension = embeddings_np.shape[1]
170
+ index = faiss.IndexFlatIP(dimension)
171
+ index.add(embeddings_np)
172
+
173
+ faiss.write_index(index, f"{directory}/invoice_index.faiss")
src/model.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from vllm import LLM
2
+ from vllm.sampling_params import SamplingParams
3
+ import base64
4
+
5
+
6
+ def encode_image(image_path: str):
7
+ with open(image_path, "rb") as image_file:
8
+ return base64.b64encode(image_file.read()).decode("utf-8")
9
+
10
+
11
+ class Pixtral:
12
+ def __init__(self, max_model_len=4096, max_tokens=4096, gpu_memory_utilization=0.95, temperature=0.35):
13
+ self.model_name = "mistralai/Pixtral-12B-2409"
14
+
15
+ self.sampling_params = SamplingParams(max_tokens=max_tokens, temperature=temperature)
16
+
17
+ self.llm = LLM(
18
+ model=self.model_name,
19
+ tokenizer_mode="mistral",
20
+ gpu_memory_utilization=gpu_memory_utilization,
21
+ load_format="mistral",
22
+ config_format="mistral",
23
+ max_model_len=max_model_len
24
+ )
25
+
26
+ def generate_message_from_image(self, prompt, image_path):
27
+ base64_image = encode_image(image_path)
28
+
29
+ messages = [
30
+ {
31
+ "role": "user",
32
+ "content": [
33
+ {"type": "text", "text": prompt},
34
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
35
+ ]
36
+ },
37
+ ]
38
+
39
+ outputs = self.llm.chat(messages, sampling_params=self.sampling_params)
40
+
41
+ return outputs[0].outputs[0].text
42
+
43
+ def generate_message(self, prompt):
44
+ messages = [
45
+ {
46
+ "role": "user",
47
+ "content": [
48
+ {"type": "text", "text": prompt},
49
+ ]
50
+ },
51
+ ]
52
+
53
+ outputs = self.llm.chat(messages, sampling_params=self.sampling_params)
54
+
55
+ return outputs[0].outputs[0].text
src/pipelines.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.RAG import RAG
2
+ from src.model import Pixtral
3
+ from src.prompts import GENERATE_INVOICE_PROMPT, GENERATE_BRIEF_DAMAGE_DESCRIPTION_PROMPT, \
4
+ GENERATE_DETAILED_DAMAGE_DESCRIPTION_PROMPT
5
+ from md2pdf.core import md2pdf
6
+ from rerankers import Reranker
7
+ import re
8
+ from fuzzywuzzy import fuzz
9
+
10
+
11
+ class InvoiceGenerator:
12
+ def __init__(
13
+ self,
14
+ fais_index_path,
15
+ image_invoice_index_path,
16
+ path_to_invoices,
17
+ path_to_images,
18
+ reranker_model=None,
19
+ device="cuda",
20
+ max_model_len=4096, max_tokens=2048, gpu_memory_utilization=0.95
21
+ ):
22
+ self.model = Pixtral(max_model_len=max_model_len, max_tokens=max_tokens,
23
+ gpu_memory_utilization=gpu_memory_utilization)
24
+ if reranker_model:
25
+ self.reranker = Reranker(model_name=reranker_model, device=device)
26
+
27
+ self.device = device
28
+ self.rag = RAG(
29
+ fais_index_path=fais_index_path,
30
+ image_invoice_index_path=image_invoice_index_path,
31
+ path_to_invoices=path_to_invoices,
32
+ path_to_images=path_to_images,
33
+ reranker=self.reranker
34
+ )
35
+ self.path_to_invoices = path_to_invoices
36
+ self.path_to_images = path_to_images
37
+
38
+ def format_invoice(self, generated_invoice, output_path, template_path="data/template.md"):
39
+ with open(template_path, "r") as f:
40
+ md_text = f.read()
41
+ md_text = md_text.replace(r"<<table>>", generated_invoice)
42
+ md2pdf(output_path, md_content=md_text)
43
+
44
+ @staticmethod
45
+ def check_within_range(generated_invoice, car_parts):
46
+ def get_part_info(part_name, car_parts):
47
+ part_name = part_name.lower()
48
+ max_match = [None, 0]
49
+ for part in car_parts:
50
+ ratio = fuzz.WRatio(part_name, part.lower())
51
+ if ratio >= 90 and ratio > max_match[1]:
52
+ max_match[0] = part
53
+ max_match[1] = ratio
54
+ return max_match[0]
55
+
56
+ all_lines = generated_invoice.split("\n")
57
+ first_cost_line = 3 if all_lines[0] == '' else 2
58
+ last_cost_line = -2 if all_lines[-1] == '' else -1
59
+ lines = generated_invoice.split("\n")[first_cost_line:last_cost_line]
60
+ cost_lines = [[line.strip() for line in cost_line.split("|")] for cost_line in lines]
61
+
62
+ comparing_results = {}
63
+
64
+ for line in cost_lines:
65
+ part = line[0]
66
+ cost = line[1]
67
+ hours = line[2]
68
+ found_part = get_part_info(part, car_parts)
69
+ if found_part:
70
+ comparing_results[part] = {
71
+ "cost_within_range": car_parts[found_part]["cost_range"][0] <= float(cost) <=
72
+ car_parts[found_part]["cost_range"][1],
73
+ "hours_within_range": car_parts[found_part]["hours_range"][0] <= float(hours) <=
74
+ car_parts[found_part]["hours_range"][1],
75
+ "cost_diff": float(cost) - car_parts[found_part]["average_cost"],
76
+ "hours_diff": float(hours) - car_parts[found_part]["average_hours"],
77
+ "part_info": found_part
78
+ }
79
+ else:
80
+ comparing_results[part] = {}
81
+
82
+ return comparing_results
83
+
84
+ @staticmethod
85
+ def check_calculations(generated_invoice):
86
+ all_lines = generated_invoice.split("\n")
87
+ first_cost_line = 3 if all_lines[0] == '' else 2
88
+ last_cost_line = -2 if all_lines[-1] == '' else -1
89
+ total_cost_line = all_lines[last_cost_line]
90
+ lines = generated_invoice.split("\n")[first_cost_line:last_cost_line]
91
+ cost_lines = [[line.strip() for line in cost_line.split("|")] for cost_line in lines]
92
+ costs = [int(line[1]) + int(line[2]) * int(line[3]) for line in cost_lines]
93
+ cost_lines = list(map(lambda x, y: [x[0], x[1], x[2], x[3], str(y)], cost_lines, costs))
94
+ total_cost = sum(costs)
95
+ total_cost_line = re.sub(r"\d+", f"{total_cost}", total_cost_line)
96
+
97
+ all_lines[last_cost_line] = total_cost_line
98
+ all_lines[first_cost_line:last_cost_line] = list(map(lambda x: " | ".join(x), cost_lines))
99
+ return "\n".join(all_lines)
100
+
101
+ def generate_invoice(self, image_path, output_path=None, template_path="data/template.md", car_parts=None):
102
+
103
+ result = {}
104
+
105
+ damage_description = self.model.generate_message_from_image(
106
+ GENERATE_BRIEF_DAMAGE_DESCRIPTION_PROMPT, image_path
107
+ )
108
+ if damage_description == "Irrelevant." or len(damage_description.split()) < 5:
109
+ return None
110
+
111
+ result["damage_description"] = damage_description
112
+
113
+ print("Damage Description:", damage_description)
114
+
115
+ invoice_info, invoice_path = self.rag.find_invoice(
116
+ image_path=image_path, return_only_path=False, damage_description=damage_description, k=5
117
+ )
118
+ invoice_info = invoice_info[0]
119
+ invoice_path = invoice_path[0]
120
+ result["invoice_info"] = invoice_info
121
+ result["invoice_path"] = invoice_path
122
+ result["similar_image"] = invoice_path.replace(".pdf", ".png")
123
+
124
+ print("Invoice Path:", invoice_path)
125
+
126
+ detailed_damage_description = self.model.generate_message_from_image(
127
+ GENERATE_DETAILED_DAMAGE_DESCRIPTION_PROMPT, image_path
128
+ )
129
+ result["detailed_damage_description"] = detailed_damage_description
130
+
131
+ print("Detailed Damage Description:", detailed_damage_description)
132
+
133
+ generated_invoice = self.model.generate_message_from_image(
134
+ GENERATE_INVOICE_PROMPT(invoice_info, detailed_damage_description), image_path
135
+ ).replace("```markdown", "").replace("```", "")
136
+ generated_invoice = self.check_calculations(generated_invoice)
137
+
138
+ result["generated_invoice"] = generated_invoice
139
+
140
+ if car_parts:
141
+ comparing_results = self.check_within_range(generated_invoice, car_parts)
142
+ result["comparing_results"] = comparing_results
143
+ print(comparing_results)
144
+
145
+ if output_path:
146
+ self.format_invoice(generated_invoice=generated_invoice, output_path=output_path,
147
+ template_path=template_path)
148
+
149
+ return result
src/prompts.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GENERATE_INVOICE_PROMPT = """
2
+ Given an image of a car accident, damages description, and an example of invoice for a similar car accident case, generate a repair invoice specifically for the provided image.
3
+ The invoice should include details on the parts needing replacement, labor hours, and costs. Structure it as a standard repair invoice typical for Bavaria Direct, in German.
4
+ Include only repair and cost information, with no contact details or extra data.
5
+
6
+ Important: Use the example invoice as a reference only. Do not copy it directly, but you can use labor costs and part names as a reference, if they are relevant to the image.
7
+
8
+ The invoice should include a list of items, where each item has the following fields:
9
+
10
+ Beschädigtes Teil (Damaged Part)
11
+ Teilkosten (Part Cost, EUR)
12
+ Arbeitsstunden (Labor Hours)
13
+ Arbeitskosten (Labor Cost per Hour, EUR/Stunde)
14
+ Gesamtkosten (Total Cost, EUR)
15
+
16
+ **Invoice for the similar car accident (for reference only):**
17
+
18
+ {0}
19
+
20
+ **Damages description**
21
+
22
+
23
+ {1}
24
+
25
+
26
+ ---
27
+
28
+ **Guidelines for Generation:**
29
+ 1. **Unique Response**: Generate reasonable variations in part types, costs, and labor hours based on the damage visible in the provided image.
30
+ 2. **Check Calculations**: Ensure that **Gesamtkosten** for each item is calculated as:
31
+ \[
32
+ Gesamtkosten = Teilkosten + (Arbeitsstunden x Arbeitskosten)
33
+ \]
34
+ The **Gesamtsumme** should reflect the sum of all **Gesamtkosten** entries.
35
+
36
+ Generate a detailed and realistic invoice tailored specifically to the image, without replicating the example.
37
+
38
+ OUTPUT REQUIREMENTS:
39
+ Your output should be a table in markdown format WITHOUT ANY ADDITIONAL COMMENTS. The format of a table is provided in reference invoice examle.
40
+ """.format
41
+
42
+
43
+ GENERATE_BRIEF_DAMAGE_DESCRIPTION_PROMPT = """
44
+ You will be provided with an image of a car accident.
45
+ If the provided photo is not a photo of a car accident or a damaged car (the picture should look like "First Notice of Loss" photo), you must write "Irrelevant." and nothing else.
46
+ If a car is completely destroyed and it is not possible to repair it, you must write "Irrelevant." and nothing else and stop following the instructions.
47
+ Otherwise, provide a brief description of the damage.
48
+ The description should include the type of damage and the affected area of the vehicle. Pay special attention to the parts of the vehicle that are damaged. Be concise and specific, focusing on the visible damage in the image.
49
+ Write no more than 1-2 sentences describing the damage in detail.
50
+ Do not add any additional information beyond the visible damage in the image and any comments.
51
+ """
52
+
53
+
54
+ GENERATE_DETAILED_DAMAGE_DESCRIPTION_PROMPT = """
55
+ Given an image of a car accident, provide a detailed and cosine description of all visible damage.
56
+ The description should include the type of damage, the affected area of the vehicle, and any other relevant details.
57
+ Be thorough and specific, covering all visible damage in the image.
58
+ Write no more than 3-4 sentences describing the damage in detail.
59
+ Do not add any additional information beyond the visible damage in the image and any comments.
60
+ """
61
+
62
+
63
+ ESTIMATE_COST_OF_CAR_PARTS_REPLACEMENT_PROMPT = """
64
+ I will provide you with a list of car parts that need to be replaced due to damage in a car accident (in German).
65
+ Your task is to estimate the cost of each part in Euros (EUR) based on the damage description provided.
66
+ Also, you need to estimate approximate labor hours required to replace each part.
67
+ There may be duplicate parts in the list (for example, for right and left sides of the car).
68
+ You may also expand the list with new parts which are not in the list but may be necessary for the repair.
69
+ Delete any duplicate entries and provide the cost and labor hours for one side only.
70
+ Sort the parts in the list in ascending order based on name of each part (in alphabetical order).
71
+ Your output should be a python dictionary where the keys are the names of the parts and the values are lists containing the estimated cost and labor hours for each part.
72
+ """