Nemm0 commited on
Commit
929c38d
·
verified ·
1 Parent(s): 7995937

Upload 4 files

Browse files
Files changed (4) hide show
  1. Project.env +3 -0
  2. Project.py +269 -0
  3. README.md +67 -20
  4. requirements.txt +8 -3
Project.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ OPENAI_API_KEY=sk-your-openai-api-key
2
+ OLLAMA_BASE_URL=
3
+ OLLAMA_MODEL=
Project.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Optional, List
3
+
4
+ import streamlit as st
5
+ from dotenv import load_dotenv
6
+
7
+
8
+ def load_env() -> None:
9
+ """Load environment variables from a local .env file if present."""
10
+ try:
11
+ load_dotenv()
12
+ except Exception:
13
+ pass
14
+
15
+
16
+ def read_text_from_file(uploaded_file) -> str:
17
+ """Read text from a Streamlit uploaded file (.txt or .pdf)."""
18
+ if uploaded_file is None:
19
+ return ""
20
+
21
+ filename = uploaded_file.name.lower()
22
+ if filename.endswith(".txt"):
23
+ raw_bytes = uploaded_file.read()
24
+ try:
25
+ return raw_bytes.decode("utf-8")
26
+ except Exception:
27
+ return raw_bytes.decode("latin-1", errors="ignore")
28
+
29
+ if filename.endswith(".pdf"):
30
+ try:
31
+ from pypdf import PdfReader
32
+ except Exception as exc:
33
+ st.error("Для чтения PDF требуется зависимость 'pypdf'. Добавьте её в окружение.")
34
+ raise exc
35
+
36
+ reader = PdfReader(uploaded_file)
37
+ pages_text: List[str] = []
38
+ for page in reader.pages:
39
+ try:
40
+ pages_text.append(page.extract_text() or "")
41
+ except Exception:
42
+ pages_text.append("")
43
+ return "\n\n".join(pages_text)
44
+
45
+ st.warning("Поддерживаются только файлы .txt и .pdf")
46
+ return ""
47
+
48
+
49
+ def make_llm(provider: str, model: str, api_key: Optional[str], temperature: float = 0.2):
50
+ """Create an LLM instance for the chosen provider."""
51
+ if provider == "OpenAI":
52
+ try:
53
+ from langchain_openai import ChatOpenAI
54
+ except Exception as exc:
55
+ st.error("Не найдена библиотека 'langchain-openai'. Установите зависимости из requirements.txt")
56
+ raise exc
57
+
58
+ effective_key = api_key or os.getenv("OPENAI_API_KEY")
59
+ if not effective_key:
60
+ st.stop()
61
+ return ChatOpenAI(model=model, api_key=effective_key, temperature=temperature)
62
+
63
+ if provider == "Ollama":
64
+ try:
65
+ from langchain_ollama import ChatOllama
66
+ except Exception as exc:
67
+ st.error("Не найдена библиотека 'langchain-ollama'. Установите зависимости из requirements.txt")
68
+ raise exc
69
+
70
+ base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
71
+ return ChatOllama(model=model, base_url=base_url, temperature=temperature)
72
+
73
+ raise ValueError(f"Unknown provider: {provider}")
74
+
75
+
76
+ def chunk_text(text: str, chunk_size: int = 2000, chunk_overlap: int = 200) -> List[str]:
77
+ """Split long text into chunks using LangChain's RecursiveCharacterTextSplitter if available.
78
+ Fallback to a simple splitter by characters.
79
+ """
80
+ text = (text or "").strip()
81
+ if not text:
82
+ return []
83
+
84
+ try:
85
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
86
+
87
+ splitter = RecursiveCharacterTextSplitter(
88
+ chunk_size=chunk_size,
89
+ chunk_overlap=chunk_overlap,
90
+ separators=["\n\n", "\n", ". ", ".", "? ", "! ", ", ", ",", " "]
91
+ )
92
+ docs = splitter.create_documents([text])
93
+ return [d.page_content for d in docs]
94
+ except Exception:
95
+ # Simple fallback: naive split by characters with safe stepping
96
+ chunks: List[str] = []
97
+ n = len(text)
98
+ safe_overlap = max(0, min(chunk_overlap, chunk_size - 1))
99
+ step = max(1, chunk_size - safe_overlap)
100
+ i = 0
101
+ while i < n:
102
+ end = min(i + chunk_size, n)
103
+ chunks.append(text[i:end])
104
+ if end >= n:
105
+ break
106
+ i += step
107
+ return chunks
108
+
109
+
110
+ def build_chunk_prompt(
111
+ chunk: str,
112
+ target_length: str,
113
+ bullet_points: bool,
114
+ language_pref: str,
115
+ ) -> str:
116
+ """Prompt to summarize a single chunk."""
117
+ formatting = (
118
+ "Сформируй маркированный список из 5-10 пунктов" if bullet_points else "Сформируй связный абзац из 5-8 предложений"
119
+ )
120
+ language_instruction = (
121
+ "Ответь на том же языке, что и входной текст." if language_pref == "Авто" else f"Ответь на {language_pref}."
122
+ )
123
+
124
+ return (
125
+ f"Ты — эксперт по конспектированию. Сожми следующий текст в {target_length} конспект для занятого читателя.\n\n"
126
+ f"Требования:\n"
127
+ f"- {formatting}\n"
128
+ f"- {language_instruction}\n"
129
+ f"- Сохраняй ключевые факты, цифры, имена, причинно-следственные связи\n"
130
+ f"- Избегай воды и повторов, не придумывай новых фактов\n\n"
131
+ f"Текст:\n{chunk}\n"
132
+ )
133
+
134
+
135
+ def build_combine_prompt(
136
+ partial_summaries: str,
137
+ target_length: str,
138
+ bullet_points: bool,
139
+ language_pref: str,
140
+ ) -> str:
141
+ formatting = (
142
+ "Сформируй маркированный список из 5-12 пунктов" if bullet_points else "Сформируй связный абзац(ы) из 8-15 предложений"
143
+ )
144
+ language_instruction = (
145
+ "Ответь на том же языке, что и входной текст." if language_pref == "Авто" else f"Ответь на {language_pref}."
146
+ )
147
+
148
+ return (
149
+ f"Ты — эксперт по сжатию информации. Объедини частичные конспекты ниже в один цельный {target_length} конспект.\n\n"
150
+ f"Требования:\n"
151
+ f"- {formatting}\n"
152
+ f"- {language_instruction}\n"
153
+ f"- Сохраняй структуру и ключевые факты без повтора\n\n"
154
+ f"Частичные конспекты:\n{partial_summaries}\n"
155
+ )
156
+
157
+
158
+ def call_llm(llm, prompt: str) -> str:
159
+ """Call the chat model with a system+user style prompt packed into a single user message."""
160
+ try:
161
+ # Many LangChain chat models accept plain strings via .invoke
162
+ result = llm.invoke(prompt)
163
+ # For Chat models, content is on .content
164
+ content = getattr(result, "content", None)
165
+ return content if isinstance(content, str) and content.strip() else (str(result) if result else "")
166
+ except Exception as exc:
167
+ st.error(f"Ошибка вызова LLM: {exc}")
168
+ raise
169
+
170
+
171
+ def summarize_long_text(
172
+ llm,
173
+ text: str,
174
+ target_length: str,
175
+ bullet_points: bool,
176
+ language_pref: str,
177
+ ) -> str:
178
+ """Chunk the text, summarize each chunk, then combine."""
179
+ chunks = chunk_text(text)
180
+ if not chunks:
181
+ return ""
182
+
183
+ if len(chunks) == 1:
184
+ single_prompt = build_chunk_prompt(chunks[0], target_length, bullet_points, language_pref)
185
+ return call_llm(llm, single_prompt)
186
+
187
+ partials: List[str] = []
188
+ for idx, ch in enumerate(chunks, start=1):
189
+ with st.spinner(f"Суммаризация фрагмента {idx}/{len(chunks)}…"):
190
+ partials.append(call_llm(llm, build_chunk_prompt(ch, target_length, bullet_points, language_pref)))
191
+
192
+ combined_prompt = build_combine_prompt("\n\n".join(partials), target_length, bullet_points, language_pref)
193
+ return call_llm(llm, combined_prompt)
194
+
195
+
196
+ def main():
197
+ load_env()
198
+
199
+ st.set_page_config(page_title="AI‑Конспектор", page_icon="📝", layout="centered")
200
+ st.title("📝 AI‑конспектор текста")
201
+ st.caption("Python + LangChain + OpenAI/Ollama + Streamlit")
202
+
203
+ with st.sidebar:
204
+ st.header("Настройки")
205
+ provider = st.selectbox("Провайдер", ["OpenAI", "Ollama"], index=0)
206
+
207
+ if provider == "OpenAI":
208
+ default_model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
209
+ model = st.selectbox("Модель (OpenAI)", ["gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"], index=0)
210
+ api_key = st.text_input("OPENAI_API_KEY", value=os.getenv("OPENAI_API_KEY", ""), type="password")
211
+ else:
212
+ default_model = os.getenv("OLLAMA_MODEL", "llama2")
213
+ model = st.text_input("Модель (Ollama)", value=default_model, help="Например: llama2, llama3, mistral")
214
+ api_key = None
215
+
216
+ target_length = st.radio(
217
+ "Длина конспекта",
218
+ options=["Короткий", "Средний", "Длинный"],
219
+ index=1,
220
+ help="Короткий ≈ 3–5 пунктов, Средний ≈ 6–10, Длинный ≈ 10–15"
221
+ )
222
+ bullet_points = st.toggle("Маркированные пункты", value=True)
223
+ language_pref = st.selectbox("Язык вывода", ["Авто", "Русский", "English"], index=0)
224
+
225
+ st.subheader("Входные данные")
226
+ tab_text, tab_file = st.tabs(["Вставить текст", "Загрузить файл (.txt/.pdf)"])
227
+
228
+ with tab_text:
229
+ input_text = st.text_area(
230
+ "Текст для конспекта",
231
+ height=240,
232
+ placeholder="Вставьте или напишите сюда длинный текст…",
233
+ ).strip()
234
+
235
+ with tab_file:
236
+ uploaded = st.file_uploader("Выберите файл", type=["txt", "pdf"], accept_multiple_files=False)
237
+ if uploaded is not None and not input_text:
238
+ input_text = read_text_from_file(uploaded)
239
+
240
+ if st.button("Сжать текст"):
241
+ if not input_text:
242
+ st.warning("Введите текст или загрузите файл.")
243
+ st.stop()
244
+
245
+ with st.spinner("Подготавливаем модель…"):
246
+ llm = make_llm(provider=provider, model=model, api_key=api_key, temperature=0.2)
247
+
248
+ with st.spinner("Генерируем конспект…"):
249
+ summary = summarize_long_text(
250
+ llm=llm,
251
+ text=input_text,
252
+ target_length=target_length,
253
+ bullet_points=bullet_points,
254
+ language_pref=language_pref,
255
+ )
256
+
257
+ if summary:
258
+ st.success("Готово!")
259
+ st.subheader("Результат")
260
+ st.write(summary)
261
+ st.download_button("⬇️ Скачать как TXT", data=summary, file_name="summary.txt")
262
+ else:
263
+ st.error("Не удалось получить конспект. Попробуйте ещё раз или измените настройки.")
264
+
265
+
266
+ if __name__ == "__main__":
267
+ main()
268
+
269
+
README.md CHANGED
@@ -1,20 +1,67 @@
1
- ---
2
- title: AI-Text Note Taker
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: AI-Text_Note_Taker
12
- license: other
13
- ---
14
-
15
- # Welcome to Streamlit!
16
-
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
18
-
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## 📝 AI‑Конспектор текста (Streamlit)
2
+
3
+ Веб‑приложение, которое сжимает длинный текст в краткий конспект. Работает с провайдерами: OpenAI (по умолчанию) и Ollama (локально, LLaMA2/LLaMA3 и др.).
4
+
5
+ ### Возможности
6
+ - Ввод текста или загрузка файлов `.txt/.pdf`
7
+ - Режимы длины: короткий / средний / длинный
8
+ - Формат: маркированные пункты или связный текст
9
+ - Язык: авто (как вход), русский, английский
10
+ - Провайдер: OpenAI (gpt-4o-mini и др.) или Ollama (llama2/llama3/mistral)
11
+
12
+ ### Установка
13
+ 1) Перейдите в папку проекта:
14
+
15
+ ```bash
16
+ cd "your project"
17
+ ```
18
+
19
+ 2) Создайте и активируйте виртуальное окружение (PowerShell):
20
+
21
+ ```bash
22
+ python -m venv .venv
23
+ .\.venv\Scripts\Activate.ps1
24
+ ```
25
+
26
+ 3) Установите зависимости:
27
+
28
+ ```bash
29
+ pip install -r requirements.txt
30
+ ```
31
+
32
+ 4) Скопируйте пример переменных окружения и пропишите ключ:
33
+
34
+ ```bash
35
+ Copy-Item .env.example .env
36
+ # Откройте .env и вставьте ваш ключ OpenAI
37
+ ```
38
+
39
+ ### Переменные окружения
40
+ Скопируйте `your project/.env.example` в `.env` и заполните при необходимости:
41
+
42
+ ```env
43
+ OPENAI_API_KEY=sk-... # ключ OpenAI (если используете OpenAI)
44
+ OLLAMA_BASE_URL=http://localhost:11434 # адрес Ollama (для локальных моделей)
45
+ OLLAMA_MODEL=llama2
46
+ ```
47
+
48
+ ### Запуск
49
+
50
+ ```bash
51
+ streamlit run "your project/Project.py"
52
+ ```
53
+
54
+ Приложение откроется в браузере. Выберите провайдера, модель и введите текст.
55
+
56
+ ### Ollama (локально, LLaMA2)
57
+ - Установите Ollama: `https://ollama.com`
58
+ - Скачайте модель: `ollama pull llama2` (или `llama3`)
59
+ - Запустите сервис (обычно запускается автоматически), проверьте `http://localhost:11434`
60
+ - В интерфейсе выберите провайдер `Ollama` и модель `llama2`
61
+
62
+ ### Стек
63
+ - Python, Streamlit
64
+ - LangChain (`langchain`, `langchain-openai`, `langchain-ollama`)
65
+ - OpenAI API или локальная Ollama (LLaMA2/3)
66
+ - pypdf для чтения PDF
67
+
requirements.txt CHANGED
@@ -1,3 +1,8 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
1
+ streamlit>=1.33.0
2
+ python-dotenv>=1.0.1
3
+ langchain>=0.1.20
4
+ langchain-openai>=0.1.7
5
+ langchain-community>=0.0.38
6
+ langchain-ollama>=0.1.0
7
+ pypdf>=4.2.0
8
+ tiktoken>=0.7.0