ofikodar commited on
Commit
bf7b232
1 Parent(s): 81db8d1

Upload 10 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.8 image as the base image
2
+ FROM python:3.8-buster
3
+
4
+ # Install the necessary dependencies
5
+ RUN apt-get update && apt-get install -y wget
6
+
7
+ # Download the wkhtmltopdf package
8
+ RUN wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.buster_amd64.deb
9
+
10
+ # Install the package
11
+ RUN apt-get install -y --no-install-recommends ./wkhtmltox_0.12.6-1.buster_amd64.deb
12
+
13
+ # Copy the requirements.txt file
14
+ COPY requirements.txt /app/
15
+
16
+ # Change the working directory
17
+ WORKDIR /app/
18
+
19
+ # Install the Python dependencies
20
+ RUN pip install -r requirements.txt
21
+
22
+ # Copy the rest of the app files
23
+ COPY src/ /app/src/
24
+ COPY app.py /app/
25
+
26
+
27
+ # Expose port 7860
28
+ EXPOSE 7860
29
+
30
+ # Set the command to run when the container starts
31
+ CMD ["python3", "-m" ,"streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.enableXsrfProtection=false"]
32
+
app.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ import pdfkit
4
+ import streamlit as st
5
+
6
+ from src.chatbot.chatgpt import Chatgpt
7
+ from src.utils import parse_pdf, build_html_resume
8
+
9
+ section_examples = {'summary': 'I have passion for new tech',
10
+ 'workExperience': 'Tell about my ability to lead projects',
11
+ 'education': 'Describe my degree type in more details'}
12
+
13
+ openai_key_info = 'https://platform.openai.com/account/api-keys'
14
+
15
+
16
+ def list_section(section_name, section_data):
17
+ description_key = 'description'
18
+
19
+ item_keys = list(section_data[0].keys())
20
+ item_keys.remove(description_key)
21
+ for item_id, section_item in enumerate(section_data):
22
+ cols = st.columns(len(item_keys))
23
+ for col, key in zip(cols, item_keys):
24
+ col.text_input(key, section_item[key], key=f'{section_name}_{item_id}_{key}')
25
+ st.text_area(description_key, section_item[description_key], key=f'{section_name}_{item_id}_{description_key}')
26
+
27
+ recruiter_subsection(section_name, section_example=section_examples[section_name], item_id=item_id)
28
+ st.markdown('***')
29
+
30
+
31
+ def skills_section(section_name, skills_data):
32
+ num_columns = 3
33
+ for skills_row in range(0, len(skills_data), num_columns):
34
+ cols = st.columns([3, 1] * num_columns)
35
+ skills_row_names = skills_data[skills_row: skills_row + num_columns]
36
+ for item_id, skill in enumerate(skills_row_names):
37
+ skill_id = skills_row + item_id
38
+ cols[item_id * 2].text_input(' ', value=skill, key=f'{section_name}_{skill_id}', label_visibility='hidden')
39
+ cols[item_id * 2 + 1].markdown('## ')
40
+ if cols[item_id * 2 + 1].button('x', key=f'{section_name}_{skill_id}_remove_skill'):
41
+ _remove_skill(skill_id, skills_data)
42
+
43
+ skill_subsection(section_name)
44
+ st.markdown('***')
45
+
46
+
47
+ def _remove_skill(skill_id, skills_data):
48
+ del skills_data[skill_id]
49
+ st.experimental_rerun()
50
+
51
+
52
+ def skill_subsection(section_name, item_id=0):
53
+ key = f'{section_name}_{item_id}_add_skill'
54
+ cols = st.columns([12, 1])
55
+ new_skill = cols[0].text_input("Add skill", key=key)
56
+ cols[1].markdown('##')
57
+ clicked = cols[1].button("\+")
58
+ if clicked and new_skill:
59
+ st.write(new_skill)
60
+ st.session_state['resume_data'][section_name].append(new_skill)
61
+ st.write(st.session_state['resume_data'][section_name])
62
+ st.experimental_rerun()
63
+
64
+
65
+ def recruiter_subsection(section_name, section_example, item_id=0):
66
+ with st.container():
67
+ cols = st.columns([3, 10], gap='small')
68
+ cols[0].write('\n')
69
+ cols[0].write('\n')
70
+ button_clicked = cols[0].button("Auto Section Improve", key=f'{section_name}_{item_id}_improve_auto')
71
+ trigger_key = 'Add a special request'
72
+ user_request_template = f"{trigger_key} to the bot here... e.g. {section_example}."
73
+
74
+ user_request = cols[1].text_input("section_example", value=user_request_template,
75
+ key=f'{section_name}_{item_id}_improve_manual', label_visibility='hidden')
76
+ if button_clicked:
77
+ user_request = '' if trigger_key in user_request else user_request
78
+ section_key = get_item_key(section_name, item_id)
79
+ section_text = st.session_state[section_key]
80
+ new_section_text = st.session_state['chatbot'].improve_section(section_text, user_request)
81
+
82
+ update_resume_data(new_section_text, section_name, item_id)
83
+ st.experimental_rerun()
84
+
85
+
86
+ def get_item_key(section_name, item_id=0):
87
+ section_key = ''
88
+ if section_name in ['workExperience', 'education']:
89
+ key = 'description'
90
+ section_key = f'{section_name}_{item_id}_{key}'
91
+ elif section_name == 'summary':
92
+ section_key = f'{section_name}'
93
+ return section_key
94
+
95
+
96
+ def update_resume_data(text_input, section_name, item_id=0):
97
+ if section_name in ['workExperience', 'education']:
98
+ key = 'description'
99
+ st.session_state['resume_data'][section_name][item_id][key] = text_input
100
+ elif section_name == 'summary':
101
+ section_key = f'{section_name}'
102
+ st.session_state['resume_data'][section_key] = text_input
103
+
104
+
105
+ def summary_section(section_name, summary_data):
106
+ st.text_area(section_name, summary_data, key=f'{section_name}', label_visibility='hidden')
107
+ recruiter_subsection(section_name, section_examples[section_name])
108
+
109
+
110
+ def contact_info_section(section_name, info_data):
111
+ for key, value in info_data.items():
112
+ if value:
113
+ st.text_input(key.title(), value, key=f'{section_name}_{key}')
114
+ st.markdown('***')
115
+
116
+
117
+ def header():
118
+ st.text_input('name', st.session_state.resume_data['name'], key="name")
119
+ st.text_input('title', st.session_state.resume_data['title'], key="title")
120
+
121
+
122
+ def body():
123
+ section_dict = {'contactInfo': contact_info_section, 'summary': summary_section, 'workExperience': list_section,
124
+ 'education': list_section, 'skills': skills_section}
125
+ tabs_names = [key_to_tab_name(key) for key in section_dict.keys()]
126
+ tabs = st.tabs(tabs_names)
127
+ for tab, key in zip(tabs, section_dict):
128
+ section_func = section_dict[key]
129
+ with tab:
130
+ section_func(key, st.session_state['resume_data'][key])
131
+
132
+
133
+ def key_to_tab_name(input_string):
134
+ return re.sub(r'([A-Z])', r' \1', input_string).strip().title()
135
+
136
+
137
+ def sidebar():
138
+ with st.sidebar:
139
+ uploaded_file = st.file_uploader('Upload PDF Resume', type="PDF")
140
+ if uploaded_file and _is_new_file(uploaded_file) and is_chatbot_loaded():
141
+ _init_resume(uploaded_file)
142
+
143
+ if is_data_loaded() and is_chatbot_loaded():
144
+ st.button("Improve More", on_click=_improve_more)
145
+ st.download_button('Download PDF', file_name='out.pdf', mime="application/json", data=download_pdf())
146
+
147
+
148
+ def download_pdf():
149
+ resume_data = format_resume_data()
150
+ html_resume = build_html_resume(resume_data)
151
+ options = {'page-size': 'A4', 'margin-top': '0.75in', 'margin-right': '0.75in', 'margin-bottom': '0.75in',
152
+ 'margin-left': '0.75in', 'encoding': "UTF-8", 'no-outline': None}
153
+ return pdfkit.from_string(html_resume, options=options, css='src/css/main.css')
154
+
155
+
156
+ def _improve_more():
157
+ print("Improving resume")
158
+ st.session_state['resume_data'] = st.session_state['chatbot'].improve_resume(st.session_state['resume_data'])
159
+
160
+
161
+ def _init_chatbot():
162
+ cols = st.columns([6, 1, 1])
163
+ api_key = cols[0].text_input("Enter OpenAI API key")
164
+ cols[1].markdown("#")
165
+ api_submit = cols[1].button("Submit")
166
+
167
+ cols[2].markdown("#")
168
+ get_info = cols[2].button("Get key")
169
+ if get_info:
170
+ st.info(f"Get your key at: {openai_key_info}")
171
+ if api_submit:
172
+ if Chatgpt.validate_api(api_key):
173
+ st.session_state['chatbot'] = Chatgpt(api_key)
174
+ st.experimental_rerun()
175
+
176
+ else:
177
+ st.error("Not valid API key - try again...")
178
+
179
+
180
+ def is_chatbot_loaded():
181
+ return st.session_state.get('chatbot')
182
+
183
+
184
+ def _is_new_file(uploaded_file):
185
+ return uploaded_file.id != st.session_state.get('file_id', '')
186
+
187
+
188
+ def _init_resume(uploaded_file):
189
+ resume_data = parse_pdf(uploaded_file)
190
+ st.session_state['resume_data'] = st.session_state['chatbot'].improve_resume(resume_data)
191
+ st.session_state['file_id'] = uploaded_file.id
192
+ st.experimental_rerun()
193
+
194
+
195
+ def format_resume_data():
196
+ current_state = st.session_state
197
+ resume_data = {}
198
+ contact_info = {}
199
+ work_experience = []
200
+ education = []
201
+ skills = []
202
+
203
+ resume_data['name'] = current_state.get('name', '')
204
+ resume_data['title'] = current_state.get('title', '')
205
+
206
+ contact_info_keys = ['linkedin', 'github', 'email', 'address']
207
+ for key in contact_info_keys:
208
+ contact_info[key] = current_state.get(f'contactInfo_{key}', '')
209
+ resume_data['contactInfo'] = contact_info
210
+
211
+ resume_data['summary'] = current_state.get('summary', '')
212
+
213
+ work_experience_keys = ['workExperience_{}_title', 'workExperience_{}_company', 'workExperience_{}_dates',
214
+ 'workExperience_{}_description']
215
+ education_keys = ['education_{}_degree', 'education_{}_school', 'education_{}_dates', 'education_{}_description']
216
+
217
+ total_work_experience = count_entries(st.session_state, 'workExperience')
218
+ total_education = count_entries(st.session_state, 'education')
219
+
220
+ for i in range(total_work_experience):
221
+ work_experience.append(
222
+ {key.split('_')[2]: current_state.get(key.format(i), '') for key in work_experience_keys})
223
+
224
+ for i in range(total_education):
225
+ education.append({key.split('_')[2]: current_state.get(key.format(i), '') for key in education_keys})
226
+
227
+ resume_data['workExperience'] = work_experience
228
+ resume_data['education'] = education
229
+
230
+ total_skills = count_entries(st.session_state, 'skills')
231
+
232
+ for i in range(total_skills):
233
+ skill_key = f'skills_{i}'
234
+
235
+ skills.append(current_state.get(skill_key, ''))
236
+ resume_data['skills'] = skills
237
+
238
+ return resume_data
239
+
240
+
241
+ def count_entries(input_dict, entry_type):
242
+ max_index = max([int(key.split("_")[1]) for key in input_dict.keys() if key.startswith(f"{entry_type}_")],
243
+ default=0)
244
+ return max_index + 1
245
+
246
+
247
+ def title():
248
+ st.title("ChatCV - AI Resume Builder")
249
+
250
+
251
+ def upload_resume_header():
252
+ st.success("Upload PDF Resume - Let the magic begin...")
253
+
254
+
255
+ def is_data_loaded():
256
+ return st.session_state.get('resume_data')
257
+
258
+
259
+ def _main():
260
+ title()
261
+ if is_chatbot_loaded():
262
+ sidebar()
263
+
264
+ if is_data_loaded():
265
+ header()
266
+ body()
267
+
268
+ else:
269
+ upload_resume_header()
270
+ else:
271
+ _init_chatbot()
272
+
273
+
274
+ if __name__ == '__main__':
275
+ _main()
276
+
277
+ # bootstrap 4 collapse example
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ revChatGPT==1.1.3
2
+ pypdf2==3.0.1
3
+ Jinja2==3.1.2
4
+ pdfkit==1.0.0
5
+ streamlit==1.15.2
src/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # from .dependency_downloader import download_spacy_model, download_nltk_corpus
2
+ #
3
+ # download_spacy_model('en_core_web_sm')
4
+ # download_nltk_corpus('words')
5
+ # download_nltk_corpus('stopwords')
src/chatbot/__init__.py ADDED
File without changes
src/chatbot/chatgpt.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import json
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Dict
6
+ import logging
7
+
8
+ import requests
9
+ from revChatGPT.Official import Chatbot
10
+
11
+ from .prompts import get_prompt, data_format
12
+
13
+ logging.basicConfig(filename='chatgpt.log', level=logging.INFO, format='%(asctime)s %(message)s',
14
+ datefmt='%m/%d/%Y %I:%M:%S %p')
15
+
16
+
17
+ class Chatgpt:
18
+ def __init__(self, api_key):
19
+ self.validate_api(api_key)
20
+ self.chatbot = Chatbot(api_key)
21
+ logging.info("API key loaded successfully")
22
+
23
+ @staticmethod
24
+ def validate_api(api_key):
25
+ if api_key and api_key.startswith("sk-") and len(api_key) > 50:
26
+ response = requests.get("https://api.openai.com/v1/engines", headers={"Authorization": f"Bearer {api_key}"})
27
+ return response.status_code == 200
28
+ return False
29
+
30
+ @staticmethod
31
+ def load_api_key(config_path):
32
+ """
33
+ Load api key from config.json
34
+
35
+ Returns:
36
+ Str: session token
37
+ """
38
+ config_file = Path(config_path)
39
+ if not config_file.is_file():
40
+ raise FileNotFoundError(f"config.json not found at {config_file.resolve()}")
41
+
42
+ with open(config_file, 'r') as j_file:
43
+ session_token = json.load(j_file)
44
+ return session_token['api_key']
45
+
46
+ def improve_resume(self, parsed_resume: str) -> Dict:
47
+ logging.info("Improving parsed resume")
48
+ chatgpt_input = get_prompt(parsed_resume, user_request='', output_type='all')
49
+ response = self._ask(chatgpt_input)
50
+ new_resume_data = self.parse_json_from_string(response)
51
+ logging.info("Resume improved successfully")
52
+ return new_resume_data
53
+
54
+ def improve_section(self, section_text, user_request=''):
55
+ logging.info("Improving section")
56
+ chatgpt_input = get_prompt(section_text, user_request=user_request, output_type='section')
57
+ response = self._ask(chatgpt_input)
58
+ new_section_text = self.clean_section_response(response)
59
+ logging.info("Section improved successfully")
60
+ return new_section_text
61
+
62
+ def _ask(self, chatgpt_input):
63
+ logging.info("Asking chatbot for response")
64
+ response = self.chatbot.ask(chatgpt_input)
65
+ answer = response['choices'][0]['text']
66
+ logging.info("Received response from chatbot")
67
+ logging.info(f"Response: {answer}")
68
+
69
+ return answer
70
+
71
+ def parse_json_from_string(self, json_string):
72
+
73
+ try:
74
+ return ast.literal_eval(json_string)
75
+ except Exception:
76
+ logging.error("Error in parsing JSON string")
77
+
78
+ json_string = re.sub('\s+', ' ', json_string)
79
+ json_string = re.sub('"', "'", json_string)
80
+ json_string = re.sub(r"(\w)'(\w)", r"\1\'\2", json_string)
81
+
82
+ clean_dict = dict()
83
+ for key, value in data_format.items():
84
+ pattern = ''
85
+ if isinstance(value, str):
86
+ pattern = f"'{key}':" + "\s*'(.*?)'"
87
+ elif isinstance(value, list):
88
+ pattern = f"'{key}':\s*(\[[^\[\]]*?\])"
89
+ elif isinstance(value, dict):
90
+ pattern = f"'{key}':" + "\s*(\{[^{}]*?\})"
91
+
92
+ extracted_value = self.extract_value(pattern, json_string)
93
+
94
+ if extracted_value:
95
+ try:
96
+ extracted_value = ast.literal_eval(extracted_value)
97
+ except Exception:
98
+ pass
99
+
100
+ if not isinstance(extracted_value, type(value)):
101
+ extracted_value = data_format[key]
102
+ clean_dict[key] = extracted_value
103
+
104
+ return clean_dict
105
+
106
+ def extract_value(self, pattern, string):
107
+ match = re.search(pattern, string)
108
+
109
+ if match:
110
+ return match.group(1)
111
+ else:
112
+ return ''
113
+
114
+ def clean_section_response(self, input_string):
115
+ try:
116
+ start = input_string.index('"')
117
+ end = input_string.rindex('"') + 1
118
+ input_string = input_string[start:end]
119
+ except ValueError:
120
+ pass
121
+ input_string = self.remove_prefix(input_string)
122
+ return input_string
123
+
124
+ @staticmethod
125
+ def remove_prefix(input_string):
126
+ return re.sub(r'\w+:\n', '', input_string)
src/chatbot/prompts.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ prompt_placeholder = '[$$$]'
2
+
3
+ data_format = {'name': '', 'title': '',
4
+ 'contactInfo': {'linkedin': '', 'github': '', 'email': '', 'address': '', 'phone': ''}, 'summary': '',
5
+ 'workExperience': [{'title': '', 'company': '', 'dates': '', 'description': ''},
6
+ {'title': '', 'company': '', 'dates': '', 'description': ''}, ],
7
+ 'education': [{'degree': '', 'school': '', 'dates': '', 'description': ''}, ], 'skills': ['', '', '']}
8
+
9
+ recruiter_prompt = 'You are a recruiter and a professional resume builder.'
10
+ command_prompt = 'Re-write the input as professionally as possible, adding vital, valuable information and skills.'
11
+ user_request_prompt = f'{prompt_placeholder}'
12
+
13
+ output_commands_prompts = dict()
14
+ output_commands_prompts[
15
+ 'all'] = f'Return the output as dictionary in the next format {str(data_format)}. Return only the keys: {str(list(data_format))}.'
16
+ output_commands_prompts['section'] = f'Return the output as string.'
17
+
18
+ input_prompt = f'Input: {prompt_placeholder}'
19
+
20
+
21
+ def get_prompt(input_data, user_request='', output_type='all'):
22
+ input_data = str(input_data)
23
+ valid_output_types = list(output_commands_prompts)
24
+ assert str(output_type) in valid_output_types, f"Not valid output type, try {valid_output_types}"
25
+
26
+ if user_request:
27
+ user_request += '\n'
28
+
29
+ template = '\n'.join(
30
+ [recruiter_prompt, command_prompt, user_request_prompt.replace(prompt_placeholder, user_request),
31
+ input_prompt.replace(prompt_placeholder, input_data), output_commands_prompts[output_type], command_prompt])
32
+ return template
src/css/main.css ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+
4
+
5
+ @page {
6
+ size: A4 portrait;
7
+
8
+ }
9
+ @media print {
10
+ header, footer {
11
+ display: none;
12
+ }
13
+ }
14
+ *, *:before, *:after {
15
+ -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;
16
+ }
17
+
18
+ html
19
+ {
20
+
21
+ font-size:100%;
22
+ }
23
+
24
+
25
+ body
26
+ {
27
+ top: 0px;
28
+ -webkit-font-smoothing:antialiased;
29
+ font-family:Lora, serif;
30
+ font-size:18px;
31
+ font-weight:500;
32
+ line-height:1.4;
33
+ text-rendering:optimizeLegibility;
34
+ }
35
+
36
+ .parent
37
+ {
38
+ }
39
+
40
+ .skill-set li:hover
41
+ {
42
+ background:#3498db;
43
+ }
44
+
45
+ h1
46
+ {
47
+ color:rgba(0,0,0,1);
48
+ }
49
+
50
+ .wrapper
51
+ {
52
+ height:100%;
53
+ }
54
+
55
+
56
+
57
+
58
+ .left
59
+ {
60
+
61
+ left: 0px;
62
+ height:100%;
63
+ margin-left:-1px;
64
+ min-width:256px;
65
+ width:70%;
66
+ float:left;
67
+ }
68
+
69
+ .right
70
+ {
71
+ right: 0px;
72
+ position: absolute;
73
+ text-align:center;
74
+ background-color:rgba(200,0,0,.025);
75
+ border-left:1px solid rgba(230,0,0,.05);
76
+
77
+ height:100%;
78
+ width:30%;
79
+ padding-left: 30px;
80
+ padding-top: 70px;
81
+
82
+ }
83
+
84
+
85
+
86
+ .name-hero
87
+ {
88
+ background:rgba(0,0,0,.001);
89
+
90
+ width:85%;
91
+ }
92
+
93
+
94
+ .name-hero h1
95
+ {
96
+ font-family:Open Sans, sans-serif;
97
+ font-size:1.5em;
98
+ text-align:center;
99
+ }
100
+
101
+ .name-hero h2
102
+ {
103
+ font-family:Open Sans, sans-serif;
104
+ font-size:1.5em;
105
+ text-align:center;
106
+ }
107
+
108
+ .name-hero h3
109
+ {
110
+ font-family:Open Sans, sans-serif;
111
+ font-size:1.5em;
112
+ text-align:center;
113
+ margin:0px auto;
114
+ }
115
+
116
+ .name-hero h1 em
117
+ {
118
+ color:rgba(0,0,0,1);
119
+ font-style:normal;
120
+ font-weight:700;
121
+ }
122
+
123
+ .name-hero p
124
+ {
125
+ color:rgba(0,0,0,1);
126
+ font-size:.75em;
127
+ line-height:1.5;
128
+ margin:0 8px 0 0;
129
+ text-align:center;
130
+ }
131
+
132
+ .name-hero .name-text
133
+ {
134
+ font-size:1.5em;
135
+ margin:0 auto;
136
+
137
+ width:85%;
138
+ }
139
+
140
+ .inner
141
+ {
142
+ margin:0 auto;
143
+ max-width:975px;
144
+ padding:0em;
145
+ }
146
+
147
+ .inner h1
148
+ {
149
+ font-size:1.75em;
150
+ }
151
+
152
+ .inner p
153
+ {
154
+ color:rgba(0,0,0,1);
155
+ }
156
+
157
+ .inner p em
158
+ {
159
+ color:rgba(0,0,0,1);
160
+ font-style:normal;
161
+ }
162
+
163
+ .inner section
164
+ {
165
+ margin:1px auto;
166
+ }
167
+
168
+ ul
169
+ {
170
+ list-style-type:none;
171
+ margin-top:0 px;
172
+ max-width:570px;
173
+ padding:1;
174
+ }
175
+
176
+ .skill-set
177
+ {
178
+
179
+ color:rgba(0,0,0,1);
180
+ list-style:none;
181
+ margin:1px 1px 0 0;
182
+ padding:10px;
183
+ text-align:justify;
184
+ }
185
+
src/templates/resume.html ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html>
2
+
3
+ <body>
4
+ <div class='parent'>
5
+ <div class="left">
6
+ <div class="inner">
7
+ <div class="name-hero">
8
+ <div class="name-text">
9
+ <h1>{{name}} </h1>
10
+ <h3>{{title}} </h3>
11
+ </div>
12
+
13
+ </div>
14
+
15
+
16
+ <div class="summary">
17
+ <h1>Summary</h1>
18
+ <p>{{summary}}</p>
19
+ </div>
20
+ <section>
21
+ <h1>Employment</h1>
22
+ {% for experience in workExperience %}
23
+ <p><em>{{experience.dates}} | {{experience.company}} | {{experience.title}}</em></p>
24
+ <p>{{experience.description}}</p>
25
+ {% endfor %}
26
+ </section>
27
+ <section>
28
+ <h1>Education</h1>
29
+ {% for edu in education %}
30
+ <p><em>{{edu.dates}} | {{edu.school}} | {{edu.degree}}</em></p>
31
+ <p>{{edu.description}}</p>
32
+
33
+ {% endfor %}
34
+ </section>
35
+
36
+
37
+ </div>
38
+ </div>
39
+ <div class="right">
40
+ <h1>Contact Information</h1>
41
+ <p>Email: {{email}}</p>
42
+ <p>Phone: {{phone}}</p>
43
+ <p>Address: {{address}}</p>
44
+ <p>LinkedIn: <a href="{{linkedin_url}}">LinkedIn Profile</a></p>
45
+ <p>Github: <a href="{{github_url}}">GitHub Profile</a></p>
46
+
47
+ <section>
48
+ <h1>Skills</h1>
49
+ <ul class="skill-set">
50
+ {% for skill in skills %}
51
+ <li>{{skill}}</li>
52
+ {% endfor %}
53
+ </ul>
54
+ </section>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </body>
59
+ </html>
src/utils.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import PyPDF2
2
+ from jinja2 import FileSystemLoader, Environment
3
+
4
+
5
+ def parse_pdf(pdf_file):
6
+ if pdf_file is isinstance(pdf_file, str):
7
+ with open(pdf_file, "rb") as file:
8
+ return _parse(file)
9
+ else:
10
+ return _parse(pdf_file)
11
+
12
+
13
+ def _parse(file):
14
+ reader = PyPDF2.PdfReader(file)
15
+ pdf_text = []
16
+ # Iterate over each page
17
+ for page_number in range(len(reader.pages)):
18
+ # Get the current page
19
+ page = reader.pages[page_number]
20
+
21
+ # Extract the text from the page
22
+ page_text = page.extract_text()
23
+
24
+ pdf_text.append(page_text)
25
+ pdf_text = '\n'.join(pdf_text)
26
+ return pdf_text
27
+
28
+
29
+ def build_html_resume(data):
30
+ env = Environment(loader=FileSystemLoader('src/templates'))
31
+ template = env.get_template('resume.html')
32
+ html_resume = template.render(data)
33
+ return html_resume
34
+
35
+
36
+ def export_html(html_resume, output_path):
37
+ with open(output_path, 'w', encoding='utf8') as f:
38
+ f.write(html_resume)