Spaces:
Sleeping
Sleeping
cover letter type feature
#1
by
aiivar
- opened
app.py
CHANGED
|
@@ -48,9 +48,16 @@ COVER_LETTER_INSTRUCTIONS = """
|
|
| 48 |
<div style="padding: 10px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 10px;">
|
| 49 |
<p><strong>Инструкция для письма:</strong></p>
|
| 50 |
<ol>
|
| 51 |
-
<li>Загрузите резюме и вакансию на первой
|
| 52 |
-
<li
|
|
|
|
| 53 |
</ol>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
</div>
|
| 55 |
"""
|
| 56 |
|
|
@@ -64,6 +71,107 @@ QA_INSTRUCTIONS = """
|
|
| 64 |
</div>
|
| 65 |
"""
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
# ============================
|
| 68 |
# Models (local transformers)
|
| 69 |
# ============================
|
|
@@ -563,6 +671,7 @@ def analyze_resume(
|
|
| 563 |
def generate_cover_letter(
|
| 564 |
resume_text: str,
|
| 565 |
job_description_text: str,
|
|
|
|
| 566 |
) -> str:
|
| 567 |
resume_text = normalize_text(resume_text)
|
| 568 |
job_description_text = normalize_text(job_description_text)
|
|
@@ -571,29 +680,35 @@ def generate_cover_letter(
|
|
| 571 |
return "❗ Сначала загрузите резюме в первой вкладке."
|
| 572 |
if not job_description_text:
|
| 573 |
return "❗ Вставьте вакансию (текстом или файлом) в первой вкладке."
|
| 574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
r_ctx, j_ctx = select_context_for_llm(resume_text, job_description_text, max_chars=6500)
|
| 576 |
-
|
| 577 |
-
prompt =
|
| 578 |
-
|
| 579 |
-
Требования:
|
| 580 |
-
- Без заголовков и списков, только текст письма.
|
| 581 |
-
- Не выдумывай опыт/технологии: используй только факты из резюме.
|
| 582 |
-
- Привяжи письмо к требованиям вакансии.
|
| 583 |
-
|
| 584 |
-
Вакансия:
|
| 585 |
-
{j_ctx}
|
| 586 |
-
|
| 587 |
-
Выдержки резюме:
|
| 588 |
-
{r_ctx}
|
| 589 |
-
"""
|
| 590 |
return groq_chat(prompt, "You are an expert in writing tailored cover letters.", LETTER_MODEL, DEFAULT_MAX_TOKENS_LETTER)
|
| 591 |
|
| 592 |
|
| 593 |
# ============================
|
| 594 |
# UI
|
| 595 |
# ============================
|
| 596 |
-
with gr.Blocks(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
gr.HTML(TITLE)
|
| 598 |
|
| 599 |
with gr.Tab("Resume Analyzer"):
|
|
@@ -638,8 +753,100 @@ with gr.Blocks() as demo:
|
|
| 638 |
|
| 639 |
with gr.Tab("Cover Letter Generator"):
|
| 640 |
gr.HTML(COVER_LETTER_INSTRUCTIONS)
|
| 641 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
cover_letter_output = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
|
| 644 |
# NEW TAB: local transformer Q&A
|
| 645 |
with gr.Tab("Q&A (Local Transformer)"):
|
|
|
|
| 48 |
<div style="padding: 10px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 10px;">
|
| 49 |
<p><strong>Инструкция для письма:</strong></p>
|
| 50 |
<ol>
|
| 51 |
+
<li>Загрузите резюме и вакансию на первой вкладке</li>
|
| 52 |
+
<li>Выберите стиль сопроводительного письма</li>
|
| 53 |
+
<li>Нажмите "Generate Cover Letter"</li>
|
| 54 |
</ol>
|
| 55 |
+
<p style="margin-top: 10px; font-size: 14px; color: #666;">
|
| 56 |
+
<strong>Совет:</strong> Попробуйте разные стили для разных типов компаний:
|
| 57 |
+
<br>• Формальный — для корпораций и банков
|
| 58 |
+
<br>• Современный — для IT-компаний и стартапов
|
| 59 |
+
<br>• Креативный — для дизайнеров, маркетологов
|
| 60 |
+
</p>
|
| 61 |
</div>
|
| 62 |
"""
|
| 63 |
|
|
|
|
| 71 |
</div>
|
| 72 |
"""
|
| 73 |
|
| 74 |
+
# ============================
|
| 75 |
+
# Cover Letter Types
|
| 76 |
+
# ============================
|
| 77 |
+
COVER_LETTER_TYPES = {
|
| 78 |
+
"formal": {
|
| 79 |
+
"name": "Формальный стиль",
|
| 80 |
+
"description": "Классическое деловое письмо, строгое и профессиональное",
|
| 81 |
+
"prompt_template": """Напиши формальное сопроводительное письмо на русском языке в деловом стиле. Требования:
|
| 82 |
+
- Используй официально-деловой стиль с уважительным обращением
|
| 83 |
+
- Структура: приветствие, представление, соответствие требованиям вакансии, заключение
|
| 84 |
+
- Объём: 8-10 предложений
|
| 85 |
+
- Избегай разговорных выражений, используй стандартные формулировки
|
| 86 |
+
- Ссылайся на конкретные требования вакансии
|
| 87 |
+
|
| 88 |
+
Вакансия:
|
| 89 |
+
{j_ctx}
|
| 90 |
+
|
| 91 |
+
Выдержки резюме:
|
| 92 |
+
{r_ctx}"""
|
| 93 |
+
},
|
| 94 |
+
"modern": {
|
| 95 |
+
"name": "Современный стиль",
|
| 96 |
+
"description": "Современный прямой стиль, популярный в IT и стартапах",
|
| 97 |
+
"prompt_template": """Напиши современное сопроводительное письмо на русском языке. Требования:
|
| 98 |
+
- Современный, прямой стиль без излишней формальности
|
| 99 |
+
- Акцент на конкретных результатах и метриках
|
| 100 |
+
- Структура: краткое введение, ключевые достижения, почему подхожу, призыв к действию
|
| 101 |
+
- 6-8 предложений, без шаблонных фраз
|
| 102 |
+
- Используй активные глаголы и конкретные примеры
|
| 103 |
+
|
| 104 |
+
Вакансия:
|
| 105 |
+
{j_ctx}
|
| 106 |
+
|
| 107 |
+
Выдержки резюме:
|
| 108 |
+
{r_ctx}"""
|
| 109 |
+
},
|
| 110 |
+
"technical": {
|
| 111 |
+
"name": "Технический фокус",
|
| 112 |
+
"description": "С акцентом на технические навыки и конкретные технологии",
|
| 113 |
+
"prompt_template": """Напиши технически ориентированное сопроводительное письмо на русском языке для IT-off специалиста. Требования:
|
| 114 |
+
- Фокус на технических навыках и технологиях из вакансии
|
| 115 |
+
- Конкретные примеры использования технологий из резюме
|
| 116 |
+
- Упоминание методологий, инструментов, фреймворков
|
| 117 |
+
- Структура: техническое соответствие, опыт работы с конкретным стеком, релевантные проекты
|
| 118 |
+
- Объём: 7-9 предложений
|
| 119 |
+
|
| 120 |
+
Вакансия:
|
| 121 |
+
{j_ctx}
|
| 122 |
+
|
| 123 |
+
Выдержки резюме:
|
| 124 |
+
{r_ctx}"""
|
| 125 |
+
},
|
| 126 |
+
"creative": {
|
| 127 |
+
"name": "Креативный подход",
|
| 128 |
+
"description": "Творческий стиль для дизайнеров, маркетологов, копирайтеров",
|
| 129 |
+
"prompt_template": """Напиши креативное сопроводительное письмо на русском языке. Требования:
|
| 130 |
+
- Креативный, нешаблонный подход, но сохраняя профессионализм
|
| 131 |
+
- Можно использовать метафоры или нестандартные сравнения (если уместно)
|
| 132 |
+
- Покажи творческий подход через структуру или формулировки
|
| 133 |
+
- Подчеркни креативные достижения и проекты
|
| 134 |
+
- Об��ём: 8-10 предложений, можно чуть больше
|
| 135 |
+
|
| 136 |
+
Вакансия:
|
| 137 |
+
{j_ctx}
|
| 138 |
+
|
| 139 |
+
Выдержки резюме:
|
| 140 |
+
{r_ctx}"""
|
| 141 |
+
},
|
| 142 |
+
"minimal": {
|
| 143 |
+
"name": "Минималистичный",
|
| 144 |
+
"description": "Краткое, по существу, без лишних слов",
|
| 145 |
+
"prompt_template": """Напиши минималистичное сопроводительное письмо на русском языке. Требования:
|
| 146 |
+
- Максимально кратко, без вводных слов и шаблонных фраз
|
| 147 |
+
- Только самое важное: соответствие требованиям, ключевой опыт
|
| 148 |
+
- 4-6 предложений, только по существу
|
| 149 |
+
- Прямой стиль, без эмоциональных окрасов
|
| 150 |
+
|
| 151 |
+
Вакансия:
|
| 152 |
+
{j_ctx}
|
| 153 |
+
|
| 154 |
+
Выдержки резюме:
|
| 155 |
+
{r_ctx}"""
|
| 156 |
+
},
|
| 157 |
+
"impact": {
|
| 158 |
+
"name": "Результато-ориентированный",
|
| 159 |
+
"description": "С акцентом на конкретные результаты и достижения",
|
| 160 |
+
"prompt_template": """Напиши сопроводительное письмо на русском с фокусом на результаты и достижения. Требования:
|
| 161 |
+
- Каждый абзац начинай с конкретного результата или достижения
|
| 162 |
+
- Используй метрики и цифры из резюме
|
| 163 |
+
- Связывай свои достижения с потребностями вакансии
|
| 164 |
+
- Структура: ключевой результат, как он достигался, как поможет компании
|
| 165 |
+
- 6-8 предложений
|
| 166 |
+
|
| 167 |
+
Вакансия:
|
| 168 |
+
{j_ctx}
|
| 169 |
+
|
| 170 |
+
Выдержки резюме:
|
| 171 |
+
{r_ctx}"""
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
# ============================
|
| 176 |
# Models (local transformers)
|
| 177 |
# ============================
|
|
|
|
| 671 |
def generate_cover_letter(
|
| 672 |
resume_text: str,
|
| 673 |
job_description_text: str,
|
| 674 |
+
letter_type: str = "modern"
|
| 675 |
) -> str:
|
| 676 |
resume_text = normalize_text(resume_text)
|
| 677 |
job_description_text = normalize_text(job_description_text)
|
|
|
|
| 680 |
return "❗ Сначала загрузите резюме в первой вкладке."
|
| 681 |
if not job_description_text:
|
| 682 |
return "❗ Вставьте вакансию (текстом или файлом) в первой вкладке."
|
| 683 |
+
|
| 684 |
+
# Проверка наличия типа письма
|
| 685 |
+
if letter_type not in COVER_LETTER_TYPES:
|
| 686 |
+
letter_type = "modern"
|
| 687 |
+
|
| 688 |
+
letter_config = COVER_LETTER_TYPES[letter_type]
|
| 689 |
+
|
| 690 |
r_ctx, j_ctx = select_context_for_llm(resume_text, job_description_text, max_chars=6500)
|
| 691 |
+
|
| 692 |
+
prompt = letter_config["prompt_template"].format(r_ctx=r_ctx, j_ctx=j_ctx)
|
| 693 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
return groq_chat(prompt, "You are an expert in writing tailored cover letters.", LETTER_MODEL, DEFAULT_MAX_TOKENS_LETTER)
|
| 695 |
|
| 696 |
|
| 697 |
# ============================
|
| 698 |
# UI
|
| 699 |
# ============================
|
| 700 |
+
with gr.Blocks(css="""
|
| 701 |
+
.style-btn { margin-right: 8px; margin-bottom: 8px; }
|
| 702 |
+
.style-btn:hover { transform: translateY(-2px); transition: all 0.2s; }
|
| 703 |
+
.cover-letter-output {
|
| 704 |
+
border: 1px solid #e5e7eb;
|
| 705 |
+
border-radius: 8px;
|
| 706 |
+
padding: 20px;
|
| 707 |
+
background-color: #fafafa;
|
| 708 |
+
white-space: pre-wrap;
|
| 709 |
+
line-height: 1.6;
|
| 710 |
+
}
|
| 711 |
+
""") as demo:
|
| 712 |
gr.HTML(TITLE)
|
| 713 |
|
| 714 |
with gr.Tab("Resume Analyzer"):
|
|
|
|
| 753 |
|
| 754 |
with gr.Tab("Cover Letter Generator"):
|
| 755 |
gr.HTML(COVER_LETTER_INSTRUCTIONS)
|
| 756 |
+
|
| 757 |
+
with gr.Row():
|
| 758 |
+
with gr.Column(scale=1):
|
| 759 |
+
letter_type = gr.Dropdown(
|
| 760 |
+
choices=[(config["name"], key) for key, config in COVER_LETTER_TYPES.items()],
|
| 761 |
+
value="modern",
|
| 762 |
+
label="Тип сопроводительного письма",
|
| 763 |
+
info="Выберите стиль письма"
|
| 764 |
+
)
|
| 765 |
+
|
| 766 |
+
# Превью описания стиля
|
| 767 |
+
type_description = gr.Markdown(
|
| 768 |
+
value=COVER_LETTER_TYPES["modern"]["description"],
|
| 769 |
+
label="Описание стиля"
|
| 770 |
+
)
|
| 771 |
+
|
| 772 |
+
# Быстрые кнопки выбора ��тиля
|
| 773 |
+
gr.Markdown("**Быстрый выбор:**")
|
| 774 |
+
with gr.Row():
|
| 775 |
+
for key in ["formal", "modern", "technical"]:
|
| 776 |
+
config = COVER_LETTER_TYPES[key]
|
| 777 |
+
btn = gr.Button(
|
| 778 |
+
config["name"],
|
| 779 |
+
size="sm",
|
| 780 |
+
variant="secondary",
|
| 781 |
+
elem_classes="style-btn"
|
| 782 |
+
)
|
| 783 |
+
btn.click(
|
| 784 |
+
lambda k=key: gr.update(value=k),
|
| 785 |
+
inputs=[],
|
| 786 |
+
outputs=[letter_type]
|
| 787 |
+
).then(
|
| 788 |
+
update_type_description,
|
| 789 |
+
inputs=[letter_type],
|
| 790 |
+
outputs=[type_description]
|
| 791 |
+
)
|
| 792 |
+
|
| 793 |
+
generate_cl_btn = gr.Button("Generate Cover Letter", variant="primary")
|
| 794 |
+
|
| 795 |
+
with gr.Column(scale=2):
|
| 796 |
+
# Быстрые превью стилей
|
| 797 |
+
gr.Markdown("### Быстрый просмотр стилей:")
|
| 798 |
+
|
| 799 |
+
style_previews = []
|
| 800 |
+
for key, config in list(COVER_LETTER_TYPES.items())[:3]: # Показываем первые 3
|
| 801 |
+
style_previews.append(
|
| 802 |
+
f"""
|
| 803 |
+
<div style="border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; margin-bottom: 10px;">
|
| 804 |
+
<div style="font-weight: 600; color: #374151;">{config['name']}</div>
|
| 805 |
+
<div style="font-size: 13px; color: #6b7280;">{config['description']}</div>
|
| 806 |
+
</div>
|
| 807 |
+
"""
|
| 808 |
+
)
|
| 809 |
+
gr.HTML("".join(style_previews))
|
| 810 |
+
|
| 811 |
+
# Обновление описания при смене типа
|
| 812 |
+
def update_type_description(letter_type_key: str):
|
| 813 |
+
config = COVER_LETTER_TYPES.get(letter_type_key, COVER_LETTER_TYPES["modern"])
|
| 814 |
+
return gr.update(value=config["description"])
|
| 815 |
+
|
| 816 |
+
letter_type.change(
|
| 817 |
+
update_type_description,
|
| 818 |
+
inputs=[letter_type],
|
| 819 |
+
outputs=[type_description]
|
| 820 |
+
)
|
| 821 |
+
|
| 822 |
+
# Отдельный блок для вывода с информацией о стиле
|
| 823 |
+
cover_letter_header = gr.Markdown("### Сгенерированное письмо:")
|
| 824 |
+
cover_letter_style_info = gr.Markdown(visible=False)
|
| 825 |
cover_letter_output = gr.Markdown()
|
| 826 |
+
|
| 827 |
+
# Функция для отображения информации о стиле
|
| 828 |
+
def show_cover_letter_with_style(letter_text: str, letter_type_key: str):
|
| 829 |
+
if letter_text.startswith("❗"):
|
| 830 |
+
return gr.update(value=""), gr.update(visible=False), gr.update(value=letter_text)
|
| 831 |
+
|
| 832 |
+
config = COVER_LETTER_TYPES.get(letter_type_key, COVER_LETTER_TYPES["modern"])
|
| 833 |
+
style_info = f"""
|
| 834 |
+
<div style="border-left: 4px solid #3b82f6; background-color: #f0f9ff; padding: 12px; border-radius: 6px; margin-bottom: 16px;">
|
| 835 |
+
<div style="font-weight: 600; color: #1e40af;">Стиль: {config['name']}</div>
|
| 836 |
+
<div style="font-size: 14px; color: #374151; margin-top: 4px;">{config['description']}</div>
|
| 837 |
+
</div>
|
| 838 |
+
"""
|
| 839 |
+
return gr.update(value=style_info), gr.update(visible=True), gr.update(value=letter_text)
|
| 840 |
+
|
| 841 |
+
generate_cl_btn.click(
|
| 842 |
+
show_cover_letter_with_style,
|
| 843 |
+
inputs=[cover_letter_output, letter_type],
|
| 844 |
+
outputs=[cover_letter_style_info, cover_letter_style_info, cover_letter_output]
|
| 845 |
+
).then(
|
| 846 |
+
generate_cover_letter,
|
| 847 |
+
inputs=[resume_content, job_description, letter_type],
|
| 848 |
+
outputs=[cover_letter_output]
|
| 849 |
+
)
|
| 850 |
|
| 851 |
# NEW TAB: local transformer Q&A
|
| 852 |
with gr.Tab("Q&A (Local Transformer)"):
|