alexandraroze commited on
Commit
87e7b64
·
1 Parent(s): 6ab63ce
Files changed (4) hide show
  1. app.py +53 -0
  2. invoice_generator.py +242 -0
  3. output/template.tex +64 -0
  4. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from PIL import Image
3
+ import os
4
+ from uuid import uuid4
5
+ import fitz
6
+ from invoice_generator import generate_invoice
7
+
8
+ st.set_page_config(page_title="Invoice generator", layout="wide")
9
+ output_folder = "output"
10
+ template = f"{output_folder}/template.tex"
11
+
12
+
13
+ def get_image_from_pdf(pdf_path):
14
+ doc = fitz.open(pdf_path)
15
+ page = doc[0]
16
+ mat = fitz.Matrix(2, 2)
17
+ pix = page.get_pixmap(matrix=mat)
18
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
19
+ return img
20
+
21
+ def display_invoice(image_path):
22
+ output_pdf = "invoice_" + os.path.basename(image_path).split(".")[0] + ".pdf"
23
+ generate_invoice(image_path, output_pdf, template, output_folder)
24
+ print(f"Generated invoice: {output_folder}/{output_pdf}")
25
+ return get_image_from_pdf(f"{output_folder}/{output_pdf}")
26
+
27
+
28
+ st.title("Upload FNOL photo")
29
+
30
+ col1, col2, col3 = st.columns([4, 1, 4])
31
+
32
+ with col1:
33
+ uploaded_image = st.file_uploader("Upload photo", type=["jpg", "jpeg", "png"])
34
+ if uploaded_image:
35
+ image = Image.open(uploaded_image)
36
+ image_path = f"{output_folder}/{str(uuid4())[:5]}.png"
37
+ image.save(image_path)
38
+ print(f"Image: {image_path}")
39
+ st.image(image, caption="Uploaded photo", width=300)
40
+ st.query_params["image"] = image_path
41
+
42
+ with col2:
43
+ if st.query_params.get("image"):
44
+ if st.button("Generate invoice"):
45
+ with st.spinner("Generating..."):
46
+ other_image = display_invoice(st.query_params["image"])
47
+ st.query_params["status"] = "loaded"
48
+ else:
49
+ st.button("Generate invoice", disabled=True)
50
+
51
+ with col3:
52
+ if st.query_params.get("status") == "loaded":
53
+ st.image(other_image, caption="Generated invoice", use_column_width=True)
invoice_generator.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+ import os
4
+ from PIL import Image
5
+ from tqdm import tqdm
6
+ import subprocess
7
+ import textwrap
8
+ import random
9
+ import fitz
10
+ import io
11
+ import uuid
12
+ from openai import AzureOpenAI, Client
13
+
14
+
15
+ def encode_image(image_path: str):
16
+ with open(image_path, "rb") as image_file:
17
+ return base64.b64encode(image_file.read()).decode("utf-8")
18
+
19
+
20
+ def generate_accident_description(client: Client, image_path: str):
21
+ base64_image = encode_image(image_path)
22
+
23
+ response = client.chat.completions.create(
24
+ model="gpt-4o",
25
+ messages=[
26
+ {
27
+ "role": "user",
28
+ "content": [
29
+ {
30
+ "type": "text",
31
+ "text": """
32
+ You will be provided with an image of a car accident.
33
+ 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.
34
+ If a car is completely destroyed and it is not possible to repair it, you must write "Irrelevant." and nothing else.
35
+ Otherwise, you must return two things:
36
+ 1) Describe in detail all the damages that you see on the car (if there are more than one, choose the most significant ones). Try to add as many details about each damage as possible.
37
+ 2) Come up with a story about how the accident happened. This story is written by a person who contacts the insurance service regarding the accident. The story should be plausible and consistent with the damages you see on the car. Write 2-3 sentences. Write it in simple words, like it was written py a person who is not a professional in car accidents.
38
+ As a result, you should return a json dictionary with the following keys: "damage_description" and "accident_story".
39
+ Remember, that you must return "Irrelevant." if the photo is not relevant.
40
+ DO NOT WRITE ANY ADDITIONAL COMMENTS.
41
+ """,
42
+ },
43
+ {
44
+ "type": "image_url",
45
+ "image_url": {
46
+ "url": f"data:image/jpeg;base64,{base64_image}",
47
+ },
48
+ },
49
+ ],
50
+ }
51
+ ],
52
+ max_tokens=1024,
53
+ temperature=0.6,
54
+ )
55
+
56
+ message = response.choices[0].message.content
57
+ if "Irrelevant" in message or len(message.split()) < 12:
58
+ return None
59
+ message = message.replace("```json", "").replace("```", "")
60
+ return json.loads(message)
61
+
62
+
63
+ def generate_invoice_file(
64
+ client: Client, image_path: str, meta_info: dict, damage_description: str
65
+ ):
66
+ base64_image = encode_image(image_path)
67
+
68
+ prompt = """
69
+ Given an image of car accident and details of damage, generate repair invoice, which includes the cost of repair, the parts that need to be replaced and the labor cost/hours.
70
+ The invoice should be a standard form typical for Bavaria Direct (write in German).
71
+ Do no write contact details or any additional information, write ONLY repair and cost information.
72
+ The invoice should include a list of items (not tables).
73
+ Each item MUST include fields: Beschädigtes Teil, "Teilkosten" (in EUR), "Arbeitsstunden" (number - hours), "Arbeitskosten" (in EUR/Stunde), "Gesamtkosten" (in EUR).
74
+ Check each calculation and make sure it is correct.
75
+
76
+ Accident details:
77
+ The accident happened in {0}, year: {1}.
78
+ Damage description:
79
+ {2}
80
+ """.format(
81
+ meta_info["location"],
82
+ meta_info["year"],
83
+ damage_description,
84
+ )
85
+
86
+ response = client.chat.completions.create(
87
+ model="gpt-4o",
88
+ messages=[
89
+ {
90
+ "role": "user",
91
+ "content": [
92
+ {"type": "text", "text": prompt},
93
+ {
94
+ "type": "image_url",
95
+ "image_url": {
96
+ "url": f"data:image/jpeg;base64,{base64_image}",
97
+ },
98
+ },
99
+ ],
100
+ }
101
+ ],
102
+ max_tokens=1024,
103
+ temperature=0.3,
104
+ )
105
+
106
+ return response.choices[0].message.content
107
+
108
+
109
+ def compile_latex(invoice_table, template, output_folder, output_pdf_name):
110
+ tex_file_path = os.path.join(output_folder, output_pdf_name.replace(".pdf", ".tex"))
111
+ output_pdf_path = os.path.join(output_folder, output_pdf_name)
112
+
113
+ with open(template, "r") as f:
114
+ latex_template = f.read()
115
+
116
+ latex_content = latex_template.replace("=========", invoice_table)
117
+
118
+ with open(tex_file_path, "w") as f:
119
+ f.write(latex_content)
120
+
121
+ try:
122
+ subprocess.run(
123
+ [
124
+ "pdflatex",
125
+ "-interaction=nonstopmode",
126
+ "-output-directory",
127
+ output_folder,
128
+ tex_file_path,
129
+ ],
130
+ stdout=subprocess.DEVNULL,
131
+ check=True,
132
+ )
133
+ if not os.path.exists(output_pdf_path):
134
+ print("PDF generation failed.")
135
+ except subprocess.CalledProcessError as e:
136
+ print(f"Error in LaTeX compilation: {e}")
137
+
138
+ extensions_to_remove = [".aux", ".out", ".log", ".tex"]
139
+ for ext in extensions_to_remove:
140
+ file_to_remove = os.path.join(
141
+ output_folder, f'{output_pdf_name.replace(".pdf", "")}{ext}'
142
+ )
143
+ if os.path.exists(file_to_remove):
144
+ os.remove(file_to_remove)
145
+
146
+
147
+ def embed_invoice_in_template(client: Client, invoice: str):
148
+ prompt = (
149
+ r"""
150
+ Given an invoice, create a latex table and write the information about Gesamtsumme below the table.
151
+ Do not add new sections or change the structure of the template (e.g. add new rows or columns).
152
+ All you can change is the content of existing table's cells and the text below the table.
153
+ In the column "Beschädigtes Teil" you should insert the corresponding text from the invoice.
154
+ In the columns "Teilkosten", "Arbeitsstunden", "Arbeitskosten", "Gesamtkosten" you should insert the corresponding INTEGER numbers from the invoice.
155
+ The invoice may contain additional information, but you should only use the information that is relevant to the table.
156
+ The invoice's field may look like "80 EUR/Stunde", but you must insert only the number "80" into the table.
157
+ YOU CAN NOT CHANGE THE COLUMN NAMES OR ADD NEW COLUMNS.
158
+ YOU OUTPUT SHOULD BE A LATEX TABLE WITHOUT ANY ADDITIONAL COMMENTS.
159
+
160
+ Table template:
161
+ ```latex
162
+ \textbf{Beschädigtes Teil} & \textbf{Teilkosten} & \textbf{Arbeitsstunden} & \textbf{Arbeitskosten} & \textbf{Gesamtkosten} \\
163
+ .... & .... & .... & .... & .... \\
164
+ ...
165
+ \hline
166
+ \end{tabular}
167
+ \vspace{2cm}
168
+ \newline
169
+ \textbf{Gesamtsumme:} .... EUR
170
+ ```
171
+
172
+ If there are more than 3 words in the "Beschädigtes Teil" field, you should add a new line between third and fourth word (new line is \\).
173
+
174
+ Invoice:
175
+
176
+ """
177
+ + invoice
178
+ )
179
+
180
+ response = client.chat.completions.create(
181
+ model="gpt-4o",
182
+ messages=[{"role": "user", "content": prompt}],
183
+ temperature=0.1,
184
+ )
185
+
186
+ return (
187
+ response.choices[0].message.content.replace("```latex", "").replace("```", "")
188
+ )
189
+
190
+
191
+ def complete_pdf(pdf_name: str, image_name: str, accident_story: str):
192
+ doc = fitz.open(pdf_name)
193
+
194
+ with Image.open(image_name) as img:
195
+ img_byte_arr = io.BytesIO()
196
+ img.save(img_byte_arr, format='PNG')
197
+ img_byte_arr.seek(0)
198
+
199
+ doc.insert_page(1)
200
+ image_rect = fitz.Rect(50, 50, 400, 400)
201
+ second_page = doc[1]
202
+ second_page.insert_image(image_rect, stream=img_byte_arr.read())
203
+ wrapped_text = textwrap.fill(accident_story, width=80)
204
+ text_position = fitz.Point(50, 420)
205
+ second_page.insert_text(text_position, wrapped_text, fontsize=12)
206
+
207
+ temp_output_name = "temp.pdf"
208
+ doc.save("temp.pdf")
209
+ doc.close()
210
+
211
+ os.replace(temp_output_name, pdf_name)
212
+
213
+
214
+ def generate_invoice(image_path, output_file, template, output_folder):
215
+ os.makedirs(output_folder, exist_ok=True)
216
+ client = AzureOpenAI(
217
+ api_key=os.environ["AZURE_API_KEY"],
218
+ api_version=os.environ["AZURE_API_VERSION"],
219
+ azure_endpoint=os.environ["AZURE_ENDPOINT"],
220
+ )
221
+ meta_info = {
222
+ "location": "Munich",
223
+ "year": 2022,
224
+ }
225
+ print("Generating description...")
226
+ description = generate_accident_description(client, image_path)
227
+ if not description:
228
+ print(f"Image {image_path} is irrelevant.")
229
+ return
230
+
231
+ assert "damage_description" in description, "damage_description not found"
232
+ assert "accident_story" in description, "accident_story not found"
233
+
234
+ print("Generating invoice...")
235
+ invoice = generate_invoice_file(client, image_path, meta_info, description["damage_description"])
236
+ invoice_table = embed_invoice_in_template(client, invoice)
237
+
238
+ print("Compiling LaTeX...")
239
+ compile_latex(invoice_table, template, output_folder, output_file)
240
+ complete_pdf(
241
+ f"{output_folder}/{output_file}", image_path, description["accident_story"]
242
+ )
output/template.tex ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \documentclass[8pt]{article}
2
+ \usepackage[a4paper, left=0.2in, right=0.3in, top=0.6in, bottom=0.6in]{geometry}
3
+ \usepackage{graphicx}
4
+ \usepackage{microtype}
5
+
6
+ \begin{document}
7
+
8
+ \noindent\textbf{\huge Rechnungsüberprüfung}
9
+
10
+ \vspace{-1cm}
11
+ \begin{flushright}
12
+ \begin{tabular}{|l|}
13
+ \hline
14
+ Schadensnummer: XXXXXX \\
15
+ Datum: XXXXXX \\
16
+ Kunde: XXXXXXXXX \\
17
+ \hline
18
+ \end{tabular}
19
+ \end{flushright}
20
+
21
+ \vspace{0.5cm}
22
+ \noindent Prozessnummer: XXXXXXXXXXXXXXXX
23
+
24
+ \vspace{0.5cm}
25
+ \noindent\begin{minipage}[t]{0.45\textwidth}
26
+ \textbf{Kunde}
27
+
28
+ \begin{tabular}{ll}
29
+ Name: & XXXXXXXXX \\
30
+ Kontakt: & XXXXXXXXX \\
31
+ Fahrzeug: & XXXXXXXXX \\
32
+ Versicherung: & XXXXXXXXX \\
33
+ Kennzeichen: & XXXXXXXXX \\
34
+ \end{tabular}
35
+ \end{minipage}
36
+ \hfill
37
+ \begin{minipage}[t]{0.45\textwidth}
38
+ \textbf{Versicherungsinformationen}
39
+
40
+ \begin{tabular}{ll}
41
+ Versicherungsgesellschaft: & XXXXXXXXX \\
42
+ Kontakt: & XXXXXXXXX \\
43
+ Adresse: & XXXXXXXXX \\
44
+ E-Mail: & XXXXXXXXX \\
45
+ \end{tabular}
46
+ \end{minipage}
47
+
48
+ \vspace{1cm}
49
+ \noindent\begin{tabular}{l r r r r}
50
+
51
+ %%\textbf{Beschädigtes Teil} & \textbf{Kosten für Ersatz} & \textbf{Arbeitsstunden} & \textbf{Arbeitskosten} & \textbf{Gesamtkosten} \\
52
+ %%.... & .... & .... & .... & .... \\
53
+ %%.... & .... & .... & .... & .... \\
54
+ %%\end{tabular}
55
+
56
+
57
+ =========
58
+
59
+ \vspace{1cm}
60
+
61
+ \noindent\textbf{Hinweis zum Kundenservice}
62
+
63
+ \end{document}
64
+
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit
2
+ pillow
3
+ tqdm
4
+ PyMuPDF
5
+ openai
6
+ !apt-get update && apt-get install -y texlive texlive-latex-extra texlive-fonts-recommended