Download app.py from PlanetExpress2125/Look4Postdoc_v.0.2: direct link, hf CLI and curl.
- Browser
- Download file 9.56 kB
-
https://huggingface.co/spaces/PlanetExpress2125/Look4Postdoc_v.0.2/resolve/main/app.py
- Command line
-
hf download hf://spaces/PlanetExpress2125/Look4Postdoc_v.0.2/app.py
-
curl -L -o app.py https://huggingface.co/spaces/PlanetExpress2125/Look4Postdoc_v.0.2/resolve/main/app.py
9.56 kB
| import os | |
| import io | |
| import numpy as np | |
| import pandas as pd | |
| import streamlit as st | |
| from sentence_transformers import SentenceTransformer | |
| from datasets import load_dataset | |
| from huggingface_hub import login | |
| # ========================== | |
| # НАСТРОЙКИ ПРИЛОЖЕНИЯ | |
| # ========================== | |
| st.set_page_config( | |
| page_title="Поиск постдока", | |
| layout="wide", | |
| ) | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| HF_MERGED_REPO = os.getenv("HF_MERGED_REPO") | |
| HF_EMB_REPO = os.getenv("HF_EMB_REPO") | |
| MODEL_NAME = os.getenv("MODEL_NAME") | |
| # Отображаемые заголовки (русские имена колонок) | |
| COLUMN_LABELS_RU = { | |
| "№": "№", | |
| "score": "Сходство", | |
| "fio": "ФИО", | |
| "title": "Название диссертации", | |
| "author_org_short": "Организация", | |
| "dissertation_type": "Тип", | |
| "protection_year": "Год", | |
| "registration_number": "Регистрационный номер", | |
| "vak_link": "Ссылка ВАК", # только в Excel | |
| } | |
| # Порядок колонок в выдаче (для интерфейса) | |
| DISPLAY_COLUMNS = [ | |
| "№", | |
| "score", | |
| "fio", | |
| "title", | |
| "author_org_short", | |
| "dissertation_type", | |
| "protection_year", | |
| "registration_number", | |
| ] | |
| if HF_TOKEN is None: | |
| st.error( | |
| "Не найден секрет `HF_TOKEN`. " | |
| "Задайте его в Settings → Variables and secrets вашего Space." | |
| ) | |
| st.stop() | |
| # Логин в Hugging Face Hub (для приватных датасетов) | |
| try: | |
| login(token=HF_TOKEN) | |
| except Exception: | |
| pass | |
| # ========================== | |
| # ЗАГРУЗКА ДАННЫХ И МОДЕЛИ | |
| # ========================== | |
| def load_data(): | |
| # 1) МЕТА-ДАННЫЕ | |
| ds_meta = load_dataset( | |
| HF_MERGED_REPO, | |
| split="train", | |
| ) | |
| df_meta = ds_meta.to_pandas() | |
| df_meta["registration_number"] = df_meta["registration_number"].astype(str) | |
| df_meta = df_meta.set_index("registration_number", drop=False) | |
| # 2) ЭМБЕДДИНГИ | |
| ds_emb = load_dataset( | |
| HF_EMB_REPO, | |
| split="train", | |
| ) | |
| df_emb = ds_emb.to_pandas() | |
| df_emb["registration_number"] = df_emb["registration_number"].astype(str) | |
| reg_nums = df_emb["registration_number"].tolist() | |
| emb_matrix = np.vstack(df_emb["embedding"].to_list()).astype("float32") | |
| norms = np.linalg.norm(emb_matrix, axis=1, keepdims=True) | |
| emb_matrix = emb_matrix / np.maximum(norms, 1e-8) | |
| return df_meta, reg_nums, emb_matrix | |
| def load_model(): | |
| model = SentenceTransformer(MODEL_NAME) | |
| return model | |
| try: | |
| df_all, reg_nums, emb_matrix = load_data() | |
| model = load_model() | |
| except Exception as e: | |
| st.error(f"Ошибка при загрузке данных или модели: {e}") | |
| st.stop() | |
| # ========================== | |
| # ЛОГИКА ПОИСКА | |
| # ========================== | |
| def search_core(query: str, top_k: int = 10): | |
| query = query.strip() | |
| if not query: | |
| return [] | |
| query_text = "query: " + query | |
| q_emb = model.encode( | |
| query_text, | |
| normalize_embeddings=True, | |
| ) | |
| scores = emb_matrix @ q_emb | |
| top_k = min(int(top_k), len(scores)) | |
| top_idx = np.argsort(-scores)[:top_k] | |
| results = [] | |
| for rank, idx in enumerate(top_idx, start=1): | |
| results.append( | |
| { | |
| "rank": rank, | |
| "registration_number": reg_nums[idx], | |
| "score": float(scores[idx]), | |
| } | |
| ) | |
| return results | |
| def extract_year(value): | |
| """Аккуратно вытаскиваем год защиты из поля protection_date.""" | |
| if value is None: | |
| return None | |
| try: | |
| if not isinstance(value, str): | |
| if pd.isna(value): | |
| return None | |
| dt = pd.to_datetime(value) | |
| return str(dt.year) | |
| except Exception: | |
| pass | |
| s = str(value).strip() | |
| if len(s) >= 4 and s[:4].isdigit(): | |
| return s[:4] | |
| return None | |
| def build_result_df(results): | |
| rows = [] | |
| for r in results: | |
| reg = r["registration_number"] | |
| score = r["score"] | |
| if reg in df_all.index: | |
| meta = df_all.loc[reg] | |
| else: | |
| meta = pd.Series({}, index=df_all.columns) | |
| protection_date = meta.get("protection_date", None) | |
| protection_year = extract_year(protection_date) | |
| row = { | |
| "№": r["rank"], | |
| "score": round(score, 4), | |
| "fio": meta.get("fio", None), | |
| "title": meta.get("title", None), | |
| "author_org_short": meta.get("author_org_short", None), | |
| "dissertation_type": meta.get("dissertation_type", None), | |
| "protection_year": protection_year, | |
| "registration_number": meta.get("registration_number", reg), | |
| "vak_link": meta.get("vak_link", ""), | |
| } | |
| rows.append(row) | |
| if not rows: | |
| return pd.DataFrame(columns=DISPLAY_COLUMNS + ["vak_link"]) | |
| df_res = pd.DataFrame(rows) | |
| df_res = df_res[DISPLAY_COLUMNS + ["vak_link"]] | |
| return df_res | |
| def run_search(query: str, top_k: int): | |
| query = query.strip() | |
| if not query: | |
| empty_df = pd.DataFrame(columns=DISPLAY_COLUMNS + ["vak_link"]) | |
| empty_df_ru = empty_df.rename(columns=COLUMN_LABELS_RU) | |
| return empty_df_ru, None | |
| results = search_core(query, top_k) | |
| df_res = build_result_df(results) | |
| if df_res.empty: | |
| empty_df_ru = df_res.rename(columns=COLUMN_LABELS_RU) | |
| return empty_df_ru, None | |
| # --- Разделяем датафреймы для отображения и для Excel --- | |
| df_excel = df_res.copy() # vak_link оставляем | |
| df_display = df_res.copy() | |
| # HTML-ссылка в колонке "Тип" | |
| def make_type_cell(row): | |
| t = row.get("dissertation_type", "") | |
| link = row.get("vak_link") or "" | |
| if isinstance(t, str) and t and link: | |
| return f'<a href="{link}" target="_blank">{t}</a>' | |
| return t | |
| df_display["dissertation_type"] = df_display.apply(make_type_cell, axis=1) | |
| df_display = df_display.drop(columns=["vak_link"]) | |
| df_display_ru = df_display.rename(columns=COLUMN_LABELS_RU) | |
| df_excel_ru = df_excel.rename(columns=COLUMN_LABELS_RU) | |
| # Excel (с отдельной колонкой "Ссылка ВАК") | |
| output = io.BytesIO() | |
| with pd.ExcelWriter(output, engine="xlsxwriter") as writer: | |
| df_excel_ru.to_excel(writer, index=False) | |
| output.seek(0) | |
| return df_display_ru, output | |
| # ========================== | |
| # UI НА STREAMLIT | |
| # ========================== | |
| # Заголовок по центру | |
| st.markdown( | |
| "<h1 style='text-align: center; margin-bottom: 0.5rem;'>Поиск постдока🎓</h1>", | |
| unsafe_allow_html=True, | |
| ) | |
| # Форма: Ctrl+Enter = отправка | |
| with st.form("search_form"): | |
| top_k = st.slider( | |
| "Сколько результатов показать", | |
| min_value=1, | |
| max_value=100, | |
| value=20, | |
| step=1, | |
| ) | |
| query = st.text_area( | |
| "Введите запрос", | |
| height=120, | |
| placeholder="Например: пластификаторы для самоуплотняющихся бетонов", | |
| key="query", | |
| ) | |
| # Кнопка по центру через три колонки | |
| c1, c2, c3 = st.columns([1, 1, 1]) | |
| with c1: | |
| st.write("") | |
| with c2: | |
| do_search = st.form_submit_button("🔍 Поиск", type="primary",use_container_width=True ) | |
| with c3: | |
| st.write("") | |
| # Результаты | |
| if do_search: | |
| with st.spinner("Идёт поиск по базе диссертаций..."): | |
| df_res_ru, excel_bytes = run_search(query, top_k) | |
| if df_res_ru.empty: | |
| st.warning("Ничего не найдено. Попробуйте изменить формулировку запроса.") | |
| else: | |
| st.success(f"Найдено записей: {len(df_res_ru)}") | |
| table_html = df_res_ru.to_html( | |
| escape=False, | |
| index=False, | |
| classes="result-table", | |
| ) | |
| st.markdown( | |
| """ | |
| <style> | |
| table.result-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| table.result-table th { | |
| text-align: center !important; | |
| vertical-align: middle; | |
| } | |
| </style> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown(table_html, unsafe_allow_html=True) | |
| if excel_bytes is not None: | |
| st.download_button( | |
| label="💾 Скачать результаты в Excel", | |
| data=excel_bytes, | |
| file_name="search_results.xlsx", | |
| mime=( | |
| "application/vnd.openxmlformats-officedocument." | |
| "spreadsheetml.sheet" | |
| ), | |
| ) | |
| else: | |
| st.info("Введите запрос выше и нажмите кнопку «Поиск» " | |
| "или нажмите Ctrl+Enter в поле ввода.") | |
| # Футер | |
| st.markdown( | |
| "<p style='font-size: 0.8rem; text-align: right; color: gray;'>" | |
| "(с) Антон Лощилов, 2025" | |
| "</p>", | |
| unsafe_allow_html=True, | |
| ) | |