BillyZ1129 commited on
Commit
5b64068
·
verified ·
1 Parent(s): 94dcd23

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +417 -23
app.py CHANGED
@@ -1,30 +1,424 @@
1
- import streamlit as st
2
- from transformers import pipeline
 
 
 
 
 
 
 
 
 
 
3
 
4
- # Load the text classification model pipeline
5
- classifier = pipeline("text-classification",model='isom5240ust/bert-base-uncased-emotion', return_all_scores=True)
 
 
6
 
7
- # Streamlit application title
8
- st.title("Text Classification for you")
9
- st.write("Classification for 6 emotions: sadness, joy, love, anger, fear, surprise")
 
 
10
 
11
- # Text input for user to enter the text to classify
12
- text = st.text_area("Enter the text to classify", "")
 
 
 
 
13
 
14
- # Perform text classification when the user clicks the "Classify" button
15
- if st.button("Classify"):
16
- # Perform text classification on the input text
17
- results = classifier(text)[0]
 
 
 
 
 
 
 
 
18
 
19
- # Display the classification result
20
- max_score = float('-inf')
21
- max_label = ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- for result in results:
24
- if result['score'] > max_score:
25
- max_score = result['score']
26
- max_label = result['label']
 
 
 
 
27
 
28
- st.write("Text:", text)
29
- st.write("Label:", max_label)
30
- st.write("Score:", max_score)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ from werkzeug.utils import secure_filename
3
+ from openai import OpenAI
4
+ from io import BytesIO
5
+ import PyPDF2
6
+ from pdfminer.high_level import extract_text
7
+ from docx import Document
8
+ import os
9
+ import re
10
+ import uuid
11
+ from typing import Tuple
12
+ import pdfplumber
13
 
14
+ app = Flask(__name__)
15
+ app.config['UPLOAD_FOLDER'] = '/home/billy1129/resume_optimizer/static/uploads'
16
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
17
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB限制
18
 
19
+ # 初始化Azure OpenAI客户端
20
+ client = OpenAI(
21
+ base_url="https://api.deepseek.com",
22
+ api_key="sk-bc73223a36d240758af12bf4a197a3be"
23
+ )
24
 
25
+ def safe_filename(filename: str) -> str:
26
+ """安全处理文件名,保留中文字符"""
27
+ filename = re.sub(r'[^\w\u4e00-\u9fff\-\.]', '', filename.strip())
28
+ name, ext = os.path.splitext(filename)
29
+ random_str = uuid.uuid4().hex[:6]
30
+ return f"{name}_{random_str}{ext}"
31
 
32
+ def extract_text_from_pdf(file_stream: BytesIO) -> str:
33
+ """混合提取方案,增强去重处理(包括标点符号和括号)"""
34
+ def process_duplicates(text: str) -> str:
35
+ """处理各种重复字符(中文、英文、标点、括号等)"""
36
+ # 处理中文重复(包括全角标点)
37
+ text = re.sub(r'([\u4e00-\u9fff])\1+', r'\1', text)
38
+ # 处理常见标点符号重复(包括全角和半角)
39
+ text = re.sub(r'([,。、;:"“”‘’\'\"\(\)\[\]\{\}\<\>])\1+', r'\1', text)
40
+ # 处理特殊重复模式(如"(("变成"(")
41
+ text = re.sub(r'(()\1+', r'\1', text)
42
+ text = re.sub(r'())\1+', r'\1', text)
43
+ return text
44
 
45
+ try:
46
+ # 优先尝试pdfplumber
47
+ try:
48
+ file_stream.seek(0)
49
+ with pdfplumber.open(file_stream) as pdf:
50
+ text = "\n".join([
51
+ page.extract_text(x_tolerance=2, y_tolerance=2)
52
+ for page in pdf.pages
53
+ if page.extract_text()
54
+ ])
55
+
56
+ print("========= pdfplumber 原始提取内容 =========")
57
+ print(text)
58
+
59
+ text = process_duplicates(text)
60
+
61
+ print("========= 去重处理后内容 =========")
62
+ print(text)
63
+ return text.strip()
64
+
65
+ except Exception as e:
66
+ print(f"pdfplumber提取失败,尝试PyPDF2: {str(e)}")
67
+ # 备用方案:PyPDF2+去重
68
+ file_stream.seek(0)
69
+ reader = PyPDF2.PdfReader(file_stream)
70
+ text = '\n'.join({
71
+ line.strip()
72
+ for page in reader.pages
73
+ for line in (page.extract_text() or "").split('\n')
74
+ if line.strip()
75
+ })
76
+
77
+ print("========= PyPDF2 原始提取内容 =========")
78
+ print(text)
79
+
80
+ text = process_duplicates(text)
81
+
82
+ print("========= 去重处理后内容 =========")
83
+ print(text)
84
+ return text
85
+
86
+ except Exception as e:
87
+ raise ValueError(f"PDF解析失败: {str(e)}")
88
 
89
+ def extract_text_from_word(file_stream: BytesIO) -> str:
90
+ """从Word文档提取文本"""
91
+ try:
92
+ file_stream.seek(0)
93
+ doc = Document(file_stream)
94
+ return "\n".join([para.text for para in doc.paragraphs if para.text])
95
+ except Exception as e:
96
+ raise ValueError(f"Word解析失败: {str(e)}")
97
 
98
+ def extract_text_from_file(file_stream: BytesIO, filename: str) -> str:
99
+ """从上传文件提取文本内容"""
100
+ if filename.lower().endswith('.pdf'):
101
+ return extract_text_from_pdf(file_stream)
102
+ elif filename.lower().endswith(('.doc', '.docx')):
103
+ return extract_text_from_word(file_stream)
104
+ elif filename.lower().endswith('.txt'):
105
+ file_stream.seek(0)
106
+ return file_stream.read().decode('utf-8', errors='ignore')
107
+ else:
108
+ raise ValueError("不支持的文件格式")
109
+
110
+ def analyze_resume_with_ai(text: str, job_position: str = None) -> Tuple[list, int]:
111
+ """使用OpenAI分析简历文本并评分"""
112
+ MAX_TOKENS = 120000
113
+ if len(text) > MAX_TOKENS * 3.5:
114
+ return ["简历内容过长,请简化内容"], 0
115
+
116
+ # 根据岗位生成针对性提示
117
+ job_specific_prompt = ""
118
+ if job_position:
119
+ job_specific_prompt = f"""
120
+ [岗位针对性分析]
121
+ 目标岗位: {job_position}
122
+ 请特别关注以下与目标岗位相关的评估维度:
123
+ 1. 专业技能匹配度: 检查简历中是否包含该岗位的核心技能关键词
124
+ 2. 项目经验相关性: 评估项目经验与目标岗位的匹配程度
125
+ 3. 行业术语使用: 检查是否使用了该岗位领域的专业术语
126
+ 4. 成就量化标准: 根据该岗位特点评估成就描述的量化程度
127
+
128
+ """
129
+ prompt = f"""请严格按照以下四部分分析简历,严格遵循格式:
130
+
131
+ {job_specific_prompt if job_specific_prompt else ""}
132
+ [总扣分]
133
+ 总扣分: XX分 # 必须单独一行明确写出总扣分值
134
+
135
+ [扣分项]
136
+ 请列出简历的所有扣分项,每一项必须明确指出扣分项在简历中的位置,扣分数量,并给出具体改进建议,将扣分项和建议放在【缺点】中输出给用户。
137
+ 请严格遵循以下评分标准中的扣分规则,最后在第一行,计算总扣分量,格式为"总扣分: XX分"。
138
+
139
+ [整体总结]
140
+ 用一段话来整体概括这篇简历的优缺点,特别关注与目标岗位的匹配度。
141
+
142
+ [优点]
143
+ • 优点1 (特别标注与目标岗位相关的优势)
144
+ • 优点2
145
+
146
+ [缺点]
147
+ • 具体位置(简历第几行或哪个部分): 具体问题 (具体改进建议) (-X分)
148
+ • 具体位置(简历第几行或哪个部分): 具体问题 (具体改进建议) (-X分)
149
+ 确保在[缺点]部分之后不输出任何其他内容
150
+
151
+ 评分标准:
152
+ 高质量简历评分标准(基于STAR法则和岗位匹配度)
153
+ 一、基础信息完整性(满分15分)
154
+ 必备信息要求:
155
+ 姓名
156
+ 联系方式(电话、邮箱)
157
+ 住址信息(至少提供省份或城市)
158
+ 评分细则:
159
+ 每项必备信息均完整且正确:得满分15分。
160
+ 缺失任一项:扣5分;如缺失两项及以上,累计扣分,但最低分为0分。
161
+
162
+ 二、内容结构与逻辑性(满分25分)
163
+ 结构要求:
164
+ 简历需清晰划分区域,如个人信息、教育经历、工作经历、技能、项目经验等。
165
+ 每一区块内容需符合逻辑,信息层次分明。
166
+ 评分细则:
167
+ 每个必备区域(至少5个区域)均明确标识并合理排序:每个区域得5分,区域缺失或模糊者扣5分。
168
+ 在每个区域内,要求描述具备逻辑性和条理性,出现明显逻辑混乱(如叙述前后矛盾或顺序混乱)者,每处扣2分,累计扣分不超过该区域分值。
169
+
170
+ 三、专业技能及关键词匹配(满分30分)
171
+ 匹配要求:
172
+ 简历中必须明确列出与目标职位直接相关的核心技能或关键词(建议不少于3项,最多5项计分)。
173
+ 评分细则:
174
+ 每列出一项与目标职位高度匹配的技能或关键词,得6分(最多计5项得分)。
175
+ 如未列出任何相关技能或关键词,直接扣30分。
176
+ 若关键词存在但与目标职位匹配度较低或描述不清晰,依据实际情况酌情扣分(每项扣分范围为2-4分)。
177
+ 【新增】岗位相关关键词匹配度额外评分:
178
+ • 完全匹配目标岗位核心技能:每项+2分(最高+10分)
179
+ • 部分匹配目标岗位次要技能:每项+1分(最高+5分)
180
+
181
+ 四、工作成就与项目描述(满分20分,必须遵循STAR法则)
182
+ 要求说明:
183
+ 每段工作经历或项目描述必须完整包含:
184
+ Situation(情境): 说明工作/项目背景与挑战。
185
+ Task(任务): 说明你在该情境下需要完成的任务。
186
+ Action(行动): 描述为解决问题所采取的具体措施。
187
+ Result(结果): 列出取得的成果和影响(最好附量化指标)。
188
+ 评分细则:
189
+ 每完整描述一项工作或项目经历且具备STAR所有要素:得5分,最多计4项得分。
190
+ 若工作或项目描述存在缺失或不清晰(例如缺少关键STAR元素),则每项扣2-5分(依据缺失程度和信息模糊程度)。
191
+ 如果简历完全没有相关描述,直接扣20分。
192
+ 【新增】岗位相关项目经验额外评分:
193
+ • 高度相关项目:每项+3分(最高+9分)
194
+ • 部分相关项目:每项+1分(最高+3分)
195
+
196
+ 五、语言表达及排版质量(满分10分)
197
+ 表达与排版要求:
198
+ 整体语言表达准确、专业,无明显错别字或语法错误。
199
+ 排版整洁、格式统一,避免混乱或信息堆砌。
200
+ 评分细则:
201
+ 排版格式符合要求,得5分;若出现明显格式错误或杂乱,每项错误扣1至5分,累计扣分最高5分。
202
+ 语言表达无错别字或语法错误,得5分;每出现一处错别字或语法错误扣1分(最多扣5分)。
203
+
204
+ 简历内容:
205
+ {text[:30000]}{'...' if len(text) > 30000 else ''}"""
206
+
207
+ try:
208
+ response = client.chat.completions.create(
209
+ model="deepseek-chat",
210
+ messages=[
211
+ {"role": "system", "content": "你是一位严格的简历评估专家。你必须严格按照评分标准进行评分和扣分,并明确指出每个缺点在简历中的具体位置。总分为100分,最终分数 = 100 - 总扣分。"},
212
+ {"role": "user", "content": prompt}
213
+ ],
214
+ temperature=1,
215
+ stream=False
216
+ )
217
+
218
+ content = response.choices[0].message.content
219
+
220
+ # 输出原始 AI 响应以便调试
221
+ print("======== RAW AI RESPONSE ========")
222
+ print(content)
223
+ print("=================================")
224
+
225
+ # 解析响应内容
226
+ lines = [line.strip() for line in content.split('\n') if line.strip()]
227
+ suggestions = []
228
+ deduction_points = 0
229
+ current_section = None
230
+
231
+ # 首先查找显式的总扣分声明
232
+ total_deduction_match = None
233
+ for line in lines:
234
+ total_deduction_match = re.search(r'总扣分[::]\s*(\d+)分', line)
235
+ if total_deduction_match:
236
+ deduction_points = int(total_deduction_match.group(1))
237
+ break
238
+
239
+ # 如果没有找到显式总扣分,则尝试从缺点部分累加
240
+ if total_deduction_match is None:
241
+ in_cons_section = False
242
+ for line in lines:
243
+ if re.match(r'^\[?缺点\]?', line, re.IGNORECASE):
244
+ in_cons_section = True
245
+ continue
246
+ if in_cons_section:
247
+ deduction_match = re.search(r'\(-(\d+)分\)', line)
248
+ if deduction_match:
249
+ deduction_points += int(deduction_match.group(1))
250
+
251
+ # 确保扣分值在合理范围内
252
+ deduction_points = max(0, min(100, deduction_points))
253
+
254
+ # 计算最终分数
255
+ score = max(0, min(100, 100 - deduction_points))
256
+ current_section = None
257
+
258
+ # 更严格的章节检测
259
+ for line in lines:
260
+ # 检测章节标题
261
+ if re.match(r'^\[?整体总结\]?', line, re.IGNORECASE):
262
+ current_section = "summary"
263
+ suggestions.append(line)
264
+ continue
265
+ elif re.match(r'^\[?优点\]?', line, re.IGNORECASE):
266
+ current_section = "pros"
267
+ suggestions.append(line)
268
+ continue
269
+ elif re.match(r'^\[?缺点\]?', line, re.IGNORECASE):
270
+ current_section = "cons"
271
+ suggestions.append(line)
272
+ continue
273
+ elif re.match(r'^\[?扣分项\]?', line, re.IGNORECASE):
274
+ current_section = "deduction"
275
+ continue
276
+
277
+ # 只保留当前章节的内容
278
+ if current_section in ["summary", "pros", "cons"]:
279
+ suggestions.append(line)
280
+
281
+ return suggestions, score
282
+
283
+ except Exception as e:
284
+ print(f"AI分析错误: {str(e)}")
285
+ return [f"AI分析时发生错误: {str(e)}"], 0
286
+
287
+ @app.route('/')
288
+ def index():
289
+ return render_template('index.html')
290
+
291
+ @app.route('/upload', methods=['POST'])
292
+ def upload_file():
293
+ if 'resume' not in request.files:
294
+ return jsonify({'error': '请选择文件上传'}), 400
295
+
296
+ file = request.files['resume']
297
+ if file.filename == '':
298
+ return jsonify({'error': '未选择文件'}), 400
299
+
300
+ try:
301
+ # 从form获取job_position
302
+ job_position = request.form.get('job_position')
303
+ if not job_position:
304
+ return jsonify({'error': '请选择目标岗位'}), 400
305
+
306
+ filename = safe_filename(file.filename)
307
+ file_stream = BytesIO(file.read())
308
+
309
+ text = extract_text_from_file(file_stream, file.filename)
310
+ if not text.strip():
311
+ return jsonify({'error': '文件内容为空或无法解析'}), 400
312
+
313
+ suggestions, score = analyze_resume_with_ai(text, job_position)
314
+
315
+ return jsonify({
316
+ 'message': '分析成功',
317
+ 'suggestions': suggestions,
318
+ 'score': score,
319
+ 'filename': filename,
320
+ 'job_position': job_position
321
+ })
322
+ except ValueError as e:
323
+ return jsonify({'error': str(e)}), 400
324
+ except Exception as e:
325
+ print(f"上传处理错误: {str(e)}")
326
+ return jsonify({'error': f'处理失败: {str(e)}'}), 500
327
+
328
+ @app.route('/generate_cover_letter', methods=['POST'])
329
+ def generate_cover_letter():
330
+ try:
331
+ data = request.json
332
+ required_fields = ['company_name', 'position', 'resume_text', 'job_description']
333
+
334
+ # 验证必填字段
335
+ for field in required_fields:
336
+ if not data.get(field):
337
+ return jsonify({'error': f'缺少必填字段: {field}'}), 400
338
+
339
+ # 构建AI提示词 - 优化版
340
+ prompt = f"""你是一位专业的职业顾问,需要根据申请人提供的简历内容撰写求职信。请严格遵守以下规则:
341
+
342
+ 1. 信息真实性原则:
343
+ - 只能使用简历中明确列出的教育背景、工作经历、项目经验和技能
344
+ - 绝对禁止添加、编造或推断简历中没有的信息
345
+ - 如果某项要求(如特定技能)在简历中未体现,不要在求职信中提及
346
+
347
+ 2. 内容要求:
348
+ [必须包含的格式要素]
349
+ - 正式商务信函格式(日期、称呼、正文、结尾敬语)
350
+ - 称呼使用"尊敬的招聘经理"(如不知道具体姓名)
351
+ - 结尾要有明确的行动号召(如期待面试机会)
352
+
353
+ [内容结构]
354
+ 第一段:明确申请职位和动机(30-50字)
355
+ 第二段:从简历中提取与职位最相关的2-3个核心优势(80-120字)
356
+ 第三段:结合公司文化和职位要求的具体匹配点(80-120字)
357
+ 第四段:礼貌结尾和行动号召(30-50字)
358
+
359
+ 3. 写作规范:
360
+ - 语言简洁专业,总字数严格控制在300-400字
361
+ - 使用主动语态和积极措辞
362
+ - 量化成果时只能使用简历中提供的数据
363
+ - 避免使用夸张或主观的描述词
364
+
365
+ 4. 特别注意:
366
+ - 如果简历中没有公司要求的关键技能或经验,不要在信中编造
367
+ - 不要假设任何简历中没有的工作职责或成就
368
+ - 不要添加简历中未列出的证书、奖项或培训经历
369
+
370
+ [申请人简历内容]
371
+ {data['resume_text'][:10000]}
372
+
373
+ [目标公司信息]
374
+ 公司名称: {data['company_name']}
375
+ 公司介绍: {data.get('company_info', '未提供')}
376
+
377
+ [申请职位]
378
+ {data['position']}
379
+
380
+ [职位描述及要求]
381
+ {data['job_description']}
382
+
383
+ [申请动机]
384
+ {data.get('motivation', '未提供')}
385
+
386
+ 请现在开始撰写求职信,严格遵循以上所有要求。"""
387
+
388
+ # 调用AI生成推荐信
389
+ response = client.chat.completions.create(
390
+ model="deepseek-chat",
391
+ messages=[
392
+ {
393
+ "role": "system",
394
+ "content": """你是一位严谨的职业顾问,专门帮助求职者撰写基于事实的求职信。
395
+ 你必须:
396
+ 1. 只使用申请人简历中明确提供的信息
397
+ 2. 绝不添加、推断或编造任何简历中没有的内容
398
+ 3. 如果简历缺少职位要求的关键资质,如实呈现而不虚构
399
+ 4. 所有成就描述必须有简历中的具体数据支持"""
400
+ },
401
+ {"role": "user", "content": prompt}
402
+ ],
403
+ temperature=0.5, # 降低创造性,提高准确性
404
+ max_tokens=2000
405
+ )
406
+
407
+ content = response.choices[0].message.content
408
+
409
+ # 后处理检查
410
+ if "简历中未提及" in content or "根据我的了解" in content:
411
+ raise ValueError("AI尝试添加简历外信息")
412
+
413
+ return jsonify({
414
+ 'success': True,
415
+ 'cover_letter': content,
416
+ 'word_count': len(content.split())
417
+ })
418
+
419
+ except Exception as e:
420
+ print(f"推荐信生成错误: {str(e)}")
421
+ return jsonify({'error': f'生成失败: {str(e)}'}), 500
422
+
423
+ if __name__ == '__main__':
424
+ app.run(debug=True)