BillyZ1129 commited on
Commit
9f48a22
·
verified ·
1 Parent(s): 5274cf6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +23 -417
app.py CHANGED
@@ -1,424 +1,30 @@
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)
 
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)