7 Commits
main ... dev

Author SHA1 Message Date
Freestyle-Play
bd01abff8b intermediate commit 2025-10-10 12:54:06 +03:00
Freestyle-Play
9272ce4123 intermediate commit 2025-10-10 12:06:00 +03:00
1e5105b7d6 fix glueAudio 2025-10-09 04:40:03 +03:00
Freestyle-Play
8ba0f89f89 intermediate commit 2025-10-08 16:07:30 +03:00
bc82628147 added API compliance to the provider 2025-10-07 00:32:19 +03:00
bac78a9cee added API compliance to the provider 2025-10-06 22:50:05 +03:00
c2019b57a7 add new LLM provider io.net gemini g4f 2025-10-06 21:17:02 +03:00
8 changed files with 275 additions and 63 deletions

10
.gitignore vendored
View File

@@ -1,5 +1,5 @@
.env .env
__pycache__/ __pycache__/
outputs/* outputs/*
venv/ venv/

20
app.py
View File

@@ -5,13 +5,11 @@ from config import *
# Подгрузка сервисов # Подгрузка сервисов
from services.llm_factory import get_llm_provider from services.llm_factory import get_llm_provider
from services.fasterWhisper import FasterWhisper
# Загрузка доп. модулей # Загрузка доп. модулей
from handlers.gradioHandler import GradioHandlers from handlers.gradioHandler import GradioHandlers
from handlers.fileHandlers import FileHandlers from handlers.fileHandlers import FileHandlers
from services.fasterWhisper import FasterWhisper
from handlers.convertMdToPdf import ConvertMdToPdf from handlers.convertMdToPdf import ConvertMdToPdf
from handlers.glueAudio import GlueAudio from handlers.glueAudio import GlueAudio
@@ -115,13 +113,20 @@ def main():
saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filename) saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filename)
saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filenamePdf) saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filenamePdf)
recognizeBtn.click(gh.handleRecognizeBtn, outputs=[recognizedText], inputs=[audioFiles, fastWhisperModel, device, compute_type, beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText, gr.State(GLUED_AUDIO_FILENAME), gr.State(OUTPUT_PATH)]) recognizeBtn.click(
gh.handleRecognizeBtn,
inputs=[audioFiles, fastWhisperModel, device, compute_type, beamSize,
vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2,
wordTimestamps, noSpeechThreshold, conditionOnPreviousText, gr.State(GLUED_AUDIO_FILENAME), gr.State(OUTPUT_PATH)],
outputs=[recognizedText],
)
# Если пайплайн включен то тогда делаем автоматически # Если пайплайн включен то тогда делаем автоматически
# автоматический пайплайн # автоматический пайплайн
recognizedText.change( recognizedText.change(
gh.generateByCondition, gh.generateByCondition,
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature, isPipelineEnabledCheckbox, gr.State("change"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)], inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature,
isPipelineEnabledCheckbox, gr.State("change"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)],
outputs=[refinedText, refinedTextMD] outputs=[refinedText, refinedTextMD]
) )
@@ -131,11 +136,12 @@ def main():
llmProvider.change( llmProvider.change(
gh.update_model_dropdown, gh.update_model_dropdown,
inputs=llmProvider, inputs=llmProvider,
outputs=llmModel outputs=[llmModel, apiKey]
) )
refineTextBtn.click( refineTextBtn.click(
gh.generateByCondition, gh.generateByCondition,
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature, isPipelineEnabledCheckbox, gr.State("click"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)], inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature,
isPipelineEnabledCheckbox, gr.State("click"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)],
outputs=[refinedText, refinedTextMD] outputs=[refinedText, refinedTextMD]
) )

View File

@@ -8,7 +8,10 @@ DEVICES = ['cpu', 'cuda']
COMPUTE_TYPE = ['auto', 'int8', 'float16', 'float32'] COMPUTE_TYPE = ['auto', 'int8', 'float16', 'float32']
# Стандартный API ключ # Стандартный API ключ
DEFAULT_API_KEY=os.getenv('API_KEY') IO_API_KEY=os.getenv('IO_API_KEY')
GEMINI_API_KEY=os.getenv('GEMINI_API_KEY')
DEFAULT_API_KEY=IO_API_KEY
# Словарь провайдеров и их моделей # Словарь провайдеров и их моделей
LLM_PROVIDERS = ['io.net', 'Gemini', 'gpt4free'] LLM_PROVIDERS = ['io.net', 'Gemini', 'gpt4free']
@@ -46,42 +49,38 @@ OUTPUT_PATH='outputs'
GLUED_AUDIO_FILENAME='glued.mp3' GLUED_AUDIO_FILENAME='glued.mp3'
DEFAULT_SYSTEM_PROMPT=''' DEFAULT_SYSTEM_PROMPT='''You are a diligent university student who has recorded a lecture as an audio file and later transcribed it into raw text.
You are a smart university student creating easy-to-understand study notes summary of lesson for a classmate who is a beginner. Your source is a raw text/audio transcript. Your task is to rewrite this unstructured transcript into a clear, logically organized, and detailed lecture summary (lecture notes).
GOAL: rewrite the information into a clear, structured summary in RUSSIAN. Guidelines:
1. Structure:
- Organize the text into a hierarchy of sections and subsections.
- Use headings, bullet points, or numbering where appropriate.
- Present the material in a logical flow (from introduction → main points → details → examples → conclusion).
KEY RULES FOR CONTENT: 2. Clarity & Cohesion:
1. **Logical Structure:** Use Markdown headers (#, ##), bullet points, and short paragraphs. - Remove filler words, repetitions, and irrelevant fragments.
2. **No "Water":** Remove filler words. Keep only practical information. - Rewrite incomplete sentences into full, grammatically correct sentences.
3. **Student Tone:** Write naturally, as if sharing notes with a friend. Avoid robotic phrases like "It is important to note". - Ensure smooth transitions between topics, making the summary feel continuous and well-connected.
KEY RULES FOR LATEX (CRITICAL FOR PYLATEXENC): 3. Depth & Detail:
1. **Math Mode:** ANY variable (like t, L, C), number in a formula, or equation MUST be wrapped in dollar signs `$`. - Capture all important concepts, definitions, examples, and explanations from the lecture.
* BAD: i(t) = i_pr + i_sv - Expand shorthand or fragmented thoughts into full, precise explanations.
* GOOD: $i(t) = i_{pr} + i_{sv}$ - Where appropriate, rephrase or clarify confusing passages for better understanding.
2. **Subscripts:** Always use curly braces `{}` for subscripts longer than one character.
* BAD: $i_pr$
* GOOD: $i_{pr}$ (or $i_{пр}$ if using cyrillic)
3. **Symbols:** Use standard LaTeX commands for symbols.
* Arrow: use `\to` (e.g., $t \to \infty$).
* Infinity: use `\infty`.
* Multiplication: use `\cdot` or just space.
4. **Consistency:** Never leave a mathematical symbol as plain text. If you mention "current i", write "ток $i$".
EXAMPLE OF DESIRED OUTPUT FORMAT: 4. Accuracy:
# Тема лекции - Preserve the lecturer’s original meaning, intent, and terminology.
## Основные понятия - Avoid adding personal opinions or new information that was not in the lecture.
* **Переходный процесс** — это когда цепь перестраивается с одного режима на другой (например, щелкнули выключателем).
* Математически это описывается дифференциальными уравнениями. Порядок уравнения = количеству реактивных элементов ($L$ и $C$).
## Классический метод 5. Style:
Решение ищется в виде суммы двух частей: - Write in a formal, academic tone suitable for study notes.
$$i(t) = i_{pr} + i_{sv}$$ - Aim for readability: concise sentences, but thorough coverage of concepts.
- Use emphasis (e.g., bold or italic text) only when it improves comprehension.
1. **Принужденная составляющая** ($i_{pr}$) — это режим, который установится в будущем, когда все успокоится ($t \to \infty$). Final Output: A cohesive, detailed, and well-structured lecture summary, suitable for later studying and revision.
2. **Свободная составляющая** ($i_{sv}$) — это то, что происходит "само по себе" из-за энергии, запасенной в $L$ и $C$. Use only russian language!
USE LATEX IN DOLLAR SIGN ($)!
*** EXTRA BIG LENTH OF CONSPECT!
STRICTLY FOLLOW THESE FORMATTING RULES. OUTPUT IN RUSSIAN. MAKE AS LONG AS POSIBLE AND AS BE GOOD!
''' '''

View File

@@ -1,11 +1,61 @@
from pydub import AudioSegment import subprocess
from pathlib import Path
class GlueAudio(): class GlueAudio():
def glue(self, audioFiles): def glue(self, audio_files: list, output_path: str, output_filename: str) -> Path:
glued = AudioSegment.empty() """
Склеивает аудиофайлы с помощью FFmpeg, используя промежуточный список файлов.
Этот метод чрезвычайно эффективен по памяти и скорости.
for audioFile in audioFiles: Args:
audio = AudioSegment.from_file(audioFile) audio_files (list): Список путей к исходным аудиофайлам.
glued += audio output_path (str): Директория для сохранения итогового файла.
output_filename (str): Имя итогового склеенного файла.
return glued Returns:
Path: Путь к созданному склеенному файлу.
"""
output_dir = Path(output_path)
output_dir.mkdir(parents=True, exist_ok=True)
final_audio_path = output_dir / output_filename
if not audio_files:
raise ValueError("Список аудиофайлов для склейки пуст.")
# 1. Формируем часть команды с входными файлами (-i file1 -i file2 ...)
input_args = []
for file_path in audio_files:
input_args.extend(['-i', str(Path(file_path).resolve())])
# 2. Формируем строку для filter_complex
num_files = len(audio_files)
stream_specifiers = "".join([f"[{i}:a]" for i in range(num_files)])
filter_complex_str = f"{stream_specifiers}concat=n={num_files}:v=0:a=1[outa]"
# 3. Собираем полную команду
command = [
'ffmpeg',
*input_args, # Распаковываем список входных файлов
'-filter_complex', filter_complex_str,
'-map', '[outa]',
'-c:a', 'libmp3lame',
'-q:a', '2',
str(final_audio_path),
'-y'
]
try:
# 4. Выполняем команду
print(f"Выполнение команды FFmpeg: {' '.join(command)}")
subprocess.run(command, check=True, capture_output=True, text=True)
print("FFmpeg успешно завершил склейку.")
except FileNotFoundError:
raise FileNotFoundError("FFmpeg не найден. Убедитесь, что он установлен и доступен в системной переменной PATH.")
except subprocess.CalledProcessError as e:
print("Ошибка при выполнении FFmpeg!")
print("Stderr:", e.stderr)
raise RuntimeError(f"Ошибка FFmpeg при склейке файлов: {e.stderr}")
return final_audio_path

View File

@@ -1,6 +1,6 @@
from config import LLM_MODELS # Импортируем словарь моделей from config import LLM_MODELS # Импортируем словарь моделей
import gradio as gr import gradio as gr
from config import GEMINI_API_KEY, IO_API_KEY
class GradioHandlers: class GradioHandlers:
def __init__(self, llm_factory, ConvertMdToPdf, FileHandlers, FasterWhisper, GlueAudio): def __init__(self, llm_factory, ConvertMdToPdf, FileHandlers, FasterWhisper, GlueAudio):
@@ -9,23 +9,39 @@ class GradioHandlers:
self.ga = GlueAudio() self.ga = GlueAudio()
self.ConvertMdToPdf = ConvertMdToPdf() self.ConvertMdToPdf = ConvertMdToPdf()
self.FasterWhisper = FasterWhisper() self.FasterWhisper = FasterWhisper()
self.llm_factory = llm_factory # Сохраняем фабрику self.llm_factory = llm_factory
def handleRecognizeBtn(self, audioFiles, model, device, compute_type, beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText, filename, outPath): def handleRecognizeBtn(
audioFile = self.ga.glue(audioFiles) self, audioFiles, model, device, compute_type, beamSize, vadFilter,
file = self.fh.saveFile(filename, audioFile, outPath) minSilenceDurationMs, speechPadMs, temp0, temp1, temp2,
wordTimestamps, noSpeechThreshold, conditionOnPreviousText, filename, outPath
):
try:
glued_audio_path = self.ga.glue(
audio_files=[f.name for f in audioFiles], # Передаем список путей
output_path=outPath,
output_filename=filename
)
except (FileNotFoundError, RuntimeError) as e:
# Если FFmpeg не найден или произошла ошибка, сообщаем пользователю
gr.Warning(str(e))
return "" # Возвращаем пустую строку в текстовое поле
return self.FasterWhisper.recognize(model, device, compute_type, file, beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText) # Передаем путь к склеенному файлу в FasterWhisper
return self.FasterWhisper.recognize(model, device, compute_type, str(glued_audio_path), beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText)
# Функция улучшения текста # Функция улучшения текста
def generateByCondition(self, api_key, llm_provider, llm_model, system_prompt, recognized_text, llm_temperature, is_pipeline_enabled, trigger, isSaveFile, filename, filenamePdf, output_path): def generateByCondition(self, api_key, llm_provider,
llm_model, system_prompt, recognized_text,
llm_temperature, is_pipeline_enabled, trigger,
isSaveFile, filename, filenamePdf, output_path):
try: try:
# Получаем нужный провайдер через фабрику # Получаем нужный провайдер через фабрику
provider = self.llm_factory(llm_provider, api_key) provider = self.llm_factory(llm_provider, api_key)
except ValueError as e: except ValueError as e:
# Если API ключ не предоставлен для нужного провайдера, выводим ошибку # Если API ключ не предоставлен для нужного провайдера, выводим ошибку
self.gr.Warning(str(e)) gr.Warning(str(e))
return self.gr.skip(), self.gr.skip() return gr.skip(), gr.skip()
def process(): def process():
result, md = provider.generate(llm_model, system_prompt, recognized_text, llm_temperature) result, md = provider.generate(llm_model, system_prompt, recognized_text, llm_temperature)
@@ -53,7 +69,9 @@ class GradioHandlers:
default_value = models[0] if models else None default_value = models[0] if models else None
# Возвращаем обновленный компонент. Используем 'gr' напрямую. # Возвращаем обновленный компонент. Используем 'gr' напрямую.
return gr.update(choices=models, value=default_value) if provider == 'io.net': return gr.update(choices=models, value=default_value), gr.update(label='API key (required for io.net, Gemini)', value=IO_API_KEY, interactive=True)
if provider == 'Gemini': return gr.update(choices=models, value=default_value), gr.update(label='API key (required for Oio.net, Gemini)', value=GEMINI_API_KEY, interactive=True)
if provider == 'gpt4free': return gr.update(choices=models, value=default_value), gr.update(label='API key (required for Oio.net, Gemini)', value="", interactive=True)
# Функция для динамического обновления кнопки # Функция для динамического обновления кнопки
def updateButton(self, isChecked): def updateButton(self, isChecked):

View File

@@ -100,3 +100,118 @@ uvicorn==0.35.0
webencodings==0.5.1 webencodings==0.5.1
websockets==15.0.1 websockets==15.0.1
zopfli==0.2.3.post1 zopfli==0.2.3.post1
aiofiles==24.1.0
aiohappyeyeballs==2.6.1
aiohttp==3.13.0
aiosignal==1.4.0
annotated-types==0.7.0
anyio==4.10.0
attrs==25.4.0
audioop-lts==0.2.2
av==15.1.0
beautifulsoup4==4.13.5
Brotli==1.1.0
bs4==0.0.2
certifi==2025.8.3
cffi==2.0.0
charset-normalizer==3.4.3
click==8.2.1
coloredlogs==15.0.1
colour==0.1.5
cssselect2==0.8.0
ctranslate2==4.6.0
distro==1.9.0
dotenv==0.9.9
exceptiongroup==1.3.0
fastapi==0.116.1
faster-whisper==1.2.0
ffmpeg-python==0.2.0
ffmpy==0.6.1
filelock==3.19.1
flatbuffers==25.2.10
flatlatex==0.15
fonttools==4.59.2
frozenlist==1.8.0
fsspec==2025.9.0
future==1.0.0
g4f==0.6.3.5
gradio==5.44.1
gradio_client==1.12.1
groovy==0.1.2
h11==0.16.0
hf-xet==1.1.9
httpcore==1.0.9
httpx==0.28.1
huggingface-hub==0.34.4
humanfriendly==10.0
idna==3.10
iso639-lang==2.6.3
Jinja2==3.1.6
jiter==0.10.0
joblib==1.5.2
langdetect==1.0.9
littleutils==0.2.4
markdown-it-py==3.0.0
markdown_pdf==1.9
MarkupSafe==3.0.2
mdurl==0.1.2
mpmath==1.3.0
multidict==6.7.0
nest-asyncio==1.6.0
nltk==3.9.1
numpy==2.2.6
onnxruntime==1.22.1
openai==1.106.1
orjson==3.11.3
outdated==0.2.2
packaging==25.0
pandas==2.3.2
pillow==11.3.0
propcache==0.4.0
protobuf==6.32.0
pycparser==2.22
pycryptodome==3.23.0
pydantic==2.11.7
pydantic_core==2.33.2
pydub==0.25.1
pydyf==0.11.0
Pygments==2.19.2
pylatexenc==2.10
pymultidictionary==1.3.2
PyMuPDF==1.26.4
pyperclip==1.9.0
pyphen==0.17.2
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-multipart==0.0.20
pytz==2025.2
PyYAML==6.0.2
regex==2025.9.1
requests==2.32.5
rich==14.1.0
ruff==0.12.12
safehttpx==0.1.6
semantic-version==2.10.0
setuptools==80.9.0
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
soupsieve==2.8
starlette==0.47.3
sympy==1.14.0
tinycss2==1.4.0
tinyhtml5==2.0.0
tkmacosx==1.0.5
tokenizers==0.22.0
tomlkit==0.13.3
tqdm==4.67.1
typer==0.17.4
typing-inspection==0.4.1
typing_extensions==4.15.0
tzdata==2025.2
urllib3==2.5.0
uvicorn==0.35.0
webencodings==0.5.1
websockets==15.0.1
yarl==1.22.0
zopfli==0.2.3.post1

View File

@@ -1,7 +1,11 @@
from faster_whisper import WhisperModel from faster_whisper import WhisperModel
class FasterWhisper: class FasterWhisper:
def recognize(self, model, device, compute_type, audioFile, beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText): def recognize(self, model, device, compute_type,
audioFile, beamSize, vadFilter,
minSilenceDurationMs, speechPadMs,
temp0, temp1, temp2, wordTimestamps,
noSpeechThreshold, conditionOnPreviousText):
model = WhisperModel(model, device=device, compute_type=compute_type) # Задаем модель model = WhisperModel(model, device=device, compute_type=compute_type) # Задаем модель
segments, _ = model.transcribe( # Распознаем текст segments, _ = model.transcribe( # Распознаем текст
@@ -22,6 +26,7 @@ class FasterWhisper:
for seg in segments: for seg in segments:
text += f"[{self.format_timestamp(seg.start)} -> {self.format_timestamp(seg.end)}] {seg.text}" + '\n' text += f"[{self.format_timestamp(seg.start)} -> {self.format_timestamp(seg.end)}] {seg.text}" + '\n'
print(f"[{self.format_timestamp(seg.start)} -> {self.format_timestamp(seg.end)}] {seg.text}")
return text return text

19
test.py Normal file
View File

@@ -0,0 +1,19 @@
from handlers.convertMdToPdf import ConvertMdToPdf
import re
# Создаем экземпляр класса
converter = ConvertMdToPdf()
# Тестовые примеры дробей
test_cases = [
r"$\frac{1}{2}$", # простая дробь
r"$\dfrac{3}{4}$", # дробь с displaystyle
r"$\frac{a}{b} + \frac{c}{d}$", # сложение дробей
r"$$\frac{x^2}{y^3}$$", # блочная дробь
r"$\frac{\partial f}{\partial x}$" # частная производная
]
for latex in test_cases:
result = converter.replace_math(re.search(r'\$\$(.*?)\$\$|\$(.*?)\$', latex))
print(f"Input: {latex}")
print(f"Output: {result}")
print("---")