add new LLM provider io.net gemini g4f

This commit is contained in:
Freestyle-Play
2025-09-25 04:27:17 +03:00
parent f2c7dcbeb6
commit e391a86cb5
17 changed files with 1411 additions and 1223 deletions

7
.gitignore vendored
View File

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

1348
LICENSE

File diff suppressed because it is too large Load Diff

118
README.md
View File

@@ -1,59 +1,59 @@
# FWAL WebUI (Faster Whisper And LLM WebUI) by swrneko # FWAL WebUI (Faster Whisper And LLM WebUI) by swrneko
<div align="center"> <div align="center">
<img src="https://count.getloli.com/get/@swrneko-faster-whisper-llm?theme=rule34"/> <img src="https://count.getloli.com/get/@swrneko-faster-whisper-llm?theme=rule34"/>
</div> </div>
## Screenshots ## Screenshots
<div align="center"> <div align="center">
<div> <div>
<img src="src/1.png" style="object-fit: cover;"/> <img src="src/1.png" style="object-fit: cover;"/>
</div> </div>
<div style="align-items: center;"> <div style="align-items: center;">
<img src="src/2.png" style="width: 49.7%; object-fit: cover;"/> <img src="src/2.png" style="width: 49.7%; object-fit: cover;"/>
<img src="src/3.png" style="width: 49.7%; object-fit: cover;"/> <img src="src/3.png" style="width: 49.7%; object-fit: cover;"/>
</div> </div>
</div> </div>
## Requirements ## Requirements
- python-conda or miniconda; - python-conda or miniconda;
- python 3.10 or above; - python 3.10 or above;
- linux (windows not tested but probably working); - linux (windows not tested but probably working);
Python requirements are listed in `requirements.txt`. Python requirements are listed in `requirements.txt`.
## Installation ## Installation
1. Clone repository: 1. Clone repository:
``` ```
git clone https://github.com/swrneko/faster-whisper-n-ionet-llm.git git clone https://github.com/swrneko/faster-whisper-n-ionet-llm.git
cd faster-whisper-n-ionet-llm cd faster-whisper-n-ionet-llm
``` ```
also (if not insatlled) also (if not insatlled)
- Insatll conda: - Insatll conda:
``` ```
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh && bash miniconda.sh wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh && bash miniconda.sh
``` ```
2. Create virtual env: 2. Create virtual env:
``` ```
conda create -n faster-whisper-n-ionet-llm python=3.10 conda create -n faster-whisper-n-ionet-llm python=3.10
conda activate faster-whisper-n-ionet-llm conda activate faster-whisper-n-ionet-llm
conda install nvidia::cudnn cuda-version=12 conda install nvidia::cudnn cuda-version=12
``` ```
3. Install requirements: 3. Install requirements:
``` ```
pip install -r requirements.txt pip install -r requirements.txt
``` ```
4. Get api key from [io.net](https://ai.io.net/ai/api-keys) and insert into `.env` file (need to create it in root of repository directory). 4. Get api key from [io.net](https://ai.io.net/ai/api-keys) and insert into `.env` file (need to create it in root of repository directory).
It should looks like this: It should looks like this:
``` ```
API_KEY='your_api_key_without_qoutes' API_KEY='your_api_key_without_qoutes'
``` ```
5. Done! Now you can just run it like that: 5. Done! Now you can just run it like that:
```shell ```shell
python app.py python app.py
``` ```

268
app.py
View File

@@ -1,123 +1,145 @@
import gradio as gr import gradio as gr
# Загрузка параметров конфигурации # Загрузка параметров конфигурации
from config import * from config import *
# Подгрузка сервисов # Подгрузка сервисов
from services.llm import Llm from services.llm_factory import get_llm_provider
# Загрузка доп. модулей
from handlers.gradioHandler import GradioHandlers
from handlers.fileHandlers import FileHandlers # Загрузка доп. модулей
from services.fasterWhisper import FasterWhisper from handlers.gradioHandler import GradioHandlers
from handlers.convertMdToPdf import ConvertMdToPdf from handlers.fileHandlers import FileHandlers
from handlers.glueAudio import GlueAudio from services.fasterWhisper import FasterWhisper
from handlers.convertMdToPdf import ConvertMdToPdf
gh = GradioHandlers(gr, Llm, ConvertMdToPdf, FileHandlers, FasterWhisper, GlueAudio) from handlers.glueAudio import GlueAudio
def main(): gh = GradioHandlers(get_llm_provider, ConvertMdToPdf, FileHandlers, FasterWhisper, GlueAudio)
with gr.Blocks() as demo:
gr.HTML(''' def main():
<div align=center> with gr.Blocks() as demo:
<h1> gr.HTML('''
Faster Whisper WebUI <div align=center>
</h1> <h1>
</div> Faster Whisper WebUI
''') </h1>
</div>
with gr.Row(): ''')
# Вкладка с основным взаимодействием
with gr.Tab('Actions'): with gr.Row():
isPipelineEnabledCheckbox = gr.Checkbox(label='is pipeline enabled', value=True, interactive=True) # Вкладка с основным взаимодействием
with gr.Tab('Actions'):
with gr.Row(): isPipelineEnabledCheckbox = gr.Checkbox(label='is pipeline enabled', value=True, interactive=True)
with gr.Accordion(label='Recognization and integration'):
with gr.Column(): with gr.Row():
audioFiles = gr.Files(label='Load audio for transcribe', type="filepath", file_types=['audio']) with gr.Accordion(label='Recognization and integration'):
images = gr.Files(label='Upload images', file_types=['image']) with gr.Column():
recognizeBtn = gr.Button('recognize and integrate', variant='primary') audioFiles = gr.Files(label='Load audio for transcribe', type="filepath")
images = gr.Files(label='Upload images', file_types=['image'])
with gr.Accordion(label='Recognized text'): recognizeBtn = gr.Button('recognize and integrate', variant='primary')
recognizedText = gr.TextArea(label='')
with gr.Accordion(label='Recognized text'):
with gr.Accordion(label='LLM'): recognizedText = gr.TextArea(label='')
with gr.Column():
refineTextBtn = gr.Button('refine text', variant='secondary', interactive=False) with gr.Accordion(label='LLM'):
with gr.Column():
with gr.Accordion(label='Refined text raw'): refineTextBtn = gr.Button('refine text', variant='secondary', interactive=False)
refinedText = gr.Textbox(label='', show_copy_button=True)
with gr.Accordion(label='Refined text raw'):
with gr.Accordion(label='Refined text md formated'): refinedText = gr.Textbox(label='', show_copy_button=True)
refinedTextMD = gr.Markdown(label='')
with gr.Accordion(label='Refined text md formated'):
# Вкладка с настройками refinedTextMD = gr.Markdown(label='')
with gr.Tab('Settings'):
with gr.Column(): # Вкладка с настройками
# Первое поле на всю ширину в акордионе настроек with gr.Tab('Settings'):
with gr.Accordion('File settings'): with gr.Column():
saveFileCheckbox = gr.Checkbox(label='save file', value=True, interactive=True) # Первое поле на всю ширину в акордионе настроек
filename = gr.Textbox(label='Output filename', value='output.txt', interactive=True) with gr.Accordion('File settings'):
filenamePdf = gr.Textbox(label='Output filename for pdf', value='output.pdf', interactive=True) saveFileCheckbox = gr.Checkbox(label='save file', value=True, interactive=True)
filename = gr.Textbox(label='Output filename', value='output.txt', interactive=True)
# Акордион настроек faster whisper filenamePdf = gr.Textbox(label='Output filename for pdf', value='output.pdf', interactive=True)
with gr.Accordion(label='Faster whisper settings'):
with gr.Row(): # Акордион настроек faster whisper
# Левая колонка в акордионе with gr.Accordion(label='Faster whisper settings'):
with gr.Column(): with gr.Row():
device = gr.Dropdown(label='Device', choices=DEVICES, value=DEVICES[1], interactive=True) # Левая колонка в акордионе
compute_type = gr.Dropdown(label='compute_type', choices=COMPUTE_TYPE, value=COMPUTE_TYPE[0], interactive=True) with gr.Column():
fastWhisperModel = gr.Dropdown(label='Model', choices=FAST_WHISPER_MODELS, value=FAST_WHISPER_MODELS[11], interactive=True) device = gr.Dropdown(label='Device', choices=DEVICES, value=DEVICES[1], interactive=True)
compute_type = gr.Dropdown(label='compute_type', choices=COMPUTE_TYPE, value=COMPUTE_TYPE[0], interactive=True)
beamSize = gr.Number(label='beam_size', value=8, interactive=True) fastWhisperModel = gr.Dropdown(label='Model', choices=FAST_WHISPER_MODELS, value=FAST_WHISPER_MODELS[11], interactive=True)
noSpeechThreshold = gr.Number(label='no_speech_threshold', value=0.5, interactive=True)
vadFilter = gr.Checkbox(label='vad_filter', value=True, interactive=True) beamSize = gr.Number(label='beam_size', value=8, interactive=True)
wordTimestamps = gr.Checkbox(label='word_timestamps', value=True, interactive=True) noSpeechThreshold = gr.Number(label='no_speech_threshold', value=0.5, interactive=True)
conditionOnPreviousText = gr.Checkbox(label='condition_on_previous_text', value=False, interactive=True) vadFilter = gr.Checkbox(label='vad_filter', value=True, interactive=True)
wordTimestamps = gr.Checkbox(label='word_timestamps', value=True, interactive=True)
# Правая колонка в акордионе conditionOnPreviousText = gr.Checkbox(label='condition_on_previous_text', value=False, interactive=True)
with gr.Column():
with gr.Accordion(label='Vad parameters'): # Правая колонка в акордионе
minSilenceDurationMs = gr.Number(label='min_silence_duration_ms', value=300, interactive=True) with gr.Column():
speechPadMs = gr.Number(label='speech_pad_ms', value=200, interactive=True) with gr.Accordion(label='Vad parameters'):
minSilenceDurationMs = gr.Number(label='min_silence_duration_ms', value=300, interactive=True)
with gr.Accordion(label='Temperature'): speechPadMs = gr.Number(label='speech_pad_ms', value=200, interactive=True)
temp0 = gr.Number(label='temp_0', value=0.0, interactive=True)
temp1 = gr.Number(label='temp_1', value=0.2, interactive=True) with gr.Accordion(label='Temperature'):
temp2 = gr.Number(label='temp_2', value=0.4, interactive=True) temp0 = gr.Number(label='temp_0', value=0.0, interactive=True)
temp1 = gr.Number(label='temp_1', value=0.2, interactive=True)
# Нижний акордион настроек для api ключа llm temp2 = gr.Number(label='temp_2', value=0.4, interactive=True)
with gr.Accordion(label='ai.io.net api settings'):
apiKey = gr.Textbox(label='API key', value=DEFAULT_API_KEY, interactive=True) # Нижний акордион настроек для api ключа llm
with gr.Accordion(label='LLM settings'):
with gr.Accordion(label='System prompt'): apiKey = gr.Textbox(label='API key (required for io.net, Gemini)', value=DEFAULT_API_KEY, interactive=True)
systemPrompt = gr.Textbox(label='', value=DEFAULT_SYSTEM_PROMPT, interactive=True)
with gr.Accordion(label='System prompt'):
with gr.Row(): systemPrompt = gr.Textbox(label='', value=DEFAULT_SYSTEM_PROMPT, interactive=True)
llmModel = gr.Dropdown(label='models', choices=LLM_MODELS, value=LLM_MODELS[1], interactive=True)
llmTemperature = gr.Number(label='Temperature', value=0.8, interactive=True ) with gr.Row():
# ВЫБОР ПРОВАЙДЕРА
isPipelineEnabledCheckbox.change(gh.updateButton, inputs=[isPipelineEnabledCheckbox], outputs=refineTextBtn) llmProvider = gr.Dropdown(
saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filename) label='LLM Provider',
saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filenamePdf) choices=LLM_PROVIDERS,
value=LLM_PROVIDERS[0],
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)]) interactive=True
)
# Если пайплайн включен то тогда делаем автоматически # СПИСОК МОДЕЛЕЙ (теперь зависит от провайдера)
# автоматический пайплайн llmModel = gr.Dropdown(
recognizedText.change( label='Models',
gh.generateByCondition, choices=LLM_MODELS[LLM_PROVIDERS[0]], # Модели для провайдера по умолчанию
inputs=[apiKey, llmModel, systemPrompt, recognizedText, llmTemperature, isPipelineEnabledCheckbox, gr.State("change"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)], value=LLM_MODELS[LLM_PROVIDERS[0]][1],
outputs=[refinedText, refinedTextMD] interactive=True
) )
llmTemperature = gr.Number(label='Temperature', value=0.8, interactive=True)
# ручной запуск по кнопке
refineTextBtn.click( isPipelineEnabledCheckbox.change(gh.updateButton, inputs=[isPipelineEnabledCheckbox], outputs=refineTextBtn)
gh.generateByCondition, saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filename)
inputs=[apiKey, llmModel, systemPrompt, recognizedText, llmTemperature, isPipelineEnabledCheckbox, gr.State("click"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)], saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filenamePdf)
outputs=[refinedText, refinedTextMD]
) 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)])
demo.launch() # Если пайплайн включен то тогда делаем автоматически
# автоматический пайплайн
if __name__ == '__main__': recognizedText.change(
main() gh.generateByCondition,
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature, isPipelineEnabledCheckbox, gr.State("change"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)],
outputs=[refinedText, refinedTextMD]
)
# ручной запуск по кнопке
llmProvider.change(
gh.update_model_dropdown,
inputs=llmProvider,
outputs=llmModel
)
refineTextBtn.click(
gh.generateByCondition,
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature, isPipelineEnabledCheckbox, gr.State("click"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)],
outputs=[refinedText, refinedTextMD]
)
demo.launch()
if __name__ == '__main__':
main()

135
config.py
View File

@@ -1,52 +1,83 @@
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
FAST_WHISPER_MODELS = ['tiny', 'base', 'small', 'medium', 'large-v1', 'large-v2', 'large-v3', 'large', 'distil-large-v2', 'distil-large-v3', 'distil-large-v3.5', 'large-v3-turbo', 'turbo'] FAST_WHISPER_MODELS = ['tiny', 'base', 'small', 'medium', 'large-v1', 'large-v2', 'large-v3', 'large', 'distil-large-v2', 'distil-large-v3', 'distil-large-v3.5', 'large-v3-turbo', 'turbo']
LLM_MODELS = ['openai/gpt-oss-120b', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'deepseek-ai/DeepSeek-R1-0528', 'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8', 'openai/gpt-oss-20b', 'Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar', 'meta-llama/Llama-3.2-90B-Vision-Instruct', 'mistralai/Mistral-Nemo-Instruct-2407', 'Qwen/Qwen2.5-VL-32B-Instruct', 'meta-llama/Llama-3.3-70B-Instruct', 'mistralai/Devstral-Small-2505', 'mistralai/Magistral-Small-2506', 'mistralai/Mistral-Large-Instruct-2411', 'CohereForAI/aya-expanse-32b'] DEVICES = ['cpu', 'cuda']
DEVICES = ['cpu', 'cuda'] COMPUTE_TYPE = ['auto', 'int8', 'float16', 'float32']
COMPUTE_TYPE = ['auto', 'int8', 'float16', 'float32']
# Стандартный API ключ
# Стандартный API ключ DEFAULT_API_KEY=os.getenv('API_KEY')
DEFAULT_API_KEY=os.getenv('API_KEY')
# Задаем выходную директорию # Словарь провайдеров и их моделей
OUTPUT_PATH='outputs' LLM_PROVIDERS = ['io.net', 'Gemini', 'gpt4free']
LLM_MODELS = {
GLUED_AUDIO_FILENAME='glued.mp3' 'io.net': [
'openai/gpt-oss-120b', 'Qwen/Qwen3-235B-A22B-Thinking-2507',
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. 'deepseek-ai/DeepSeek-R1-0528', 'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8',
Your task is to rewrite this unstructured transcript into a clear, logically organized, and detailed lecture summary (lecture notes). 'openai/gpt-oss-20b', 'Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar',
'meta-llama/Llama-3.2-90B-Vision-Instruct', 'mistralai/Mistral-Nemo-Instruct-2407',
Guidelines: 'Qwen/Qwen2.5-VL-32B-Instruct', 'meta-llama/Llama-3.3-70B-Instruct',
1. Structure: 'mistralai/Devstral-Small-2505', 'mistralai/Magistral-Small-2506',
- Organize the text into a hierarchy of sections and subsections. 'mistralai/Mistral-Large-Instruct-2411', 'CohereForAI/aya-expanse-32b'
- Use headings, bullet points, or numbering where appropriate. ],
- Present the material in a logical flow (from introduction → main points → details → examples → conclusion). 'Gemini': [
'gemini-2.5-pro',
2. Clarity & Cohesion: 'gemini-2.5-flash',
- Remove filler words, repetitions, and irrelevant fragments. 'gemini-2.5-flash-lite'
- Rewrite incomplete sentences into full, grammatically correct sentences. ],
- Ensure smooth transitions between topics, making the summary feel continuous and well-connected. 'gpt4free': [ # Модели могут меняться, проверьте документацию g4f
'default',
3. Depth & Detail: 'gpt-4',
- Capture all important concepts, definitions, examples, and explanations from the lecture. 'sonar-reasoning',
- Expand shorthand or fragmented thoughts into full, precise explanations. 'command-r-plus',
- Where appropriate, rephrase or clarify confusing passages for better understanding. 'llama-3.3-70b',
'hermes-3-llama-3.1-405b'
4. Accuracy: 'qwen-3-235b',
- Preserve the lecturer’s original meaning, intent, and terminology. 'gpt-4o-mini',
- Avoid adding personal opinions or new information that was not in the lecture. 'deepseek-r1',
'PollinationsAI:gpt-5-nano'
5. Style: ]
- Write in a formal, academic tone suitable for study notes. }
- Aim for readability: concise sentences, but thorough coverage of concepts.
- Use emphasis (e.g., bold or italic text) only when it improves comprehension. # Задаем выходную директорию
OUTPUT_PATH='outputs'
Final Output: A cohesive, detailed, and well-structured lecture summary, suitable for later studying and revision.
Use only russian language! GLUED_AUDIO_FILENAME='glued.mp3'
USE LATEX IN DOLLAR SIGN ($)!
EXTRA BIG LENTH OF CONSPECT! 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.
MAKE AS LONG AS POSIBLE AND AS BE GOOD! Your task is to rewrite this unstructured transcript into a clear, logically organized, and detailed lecture summary (lecture notes).
'''
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).
2. Clarity & Cohesion:
- Remove filler words, repetitions, and irrelevant fragments.
- Rewrite incomplete sentences into full, grammatically correct sentences.
- Ensure smooth transitions between topics, making the summary feel continuous and well-connected.
3. Depth & Detail:
- Capture all important concepts, definitions, examples, and explanations from the lecture.
- Expand shorthand or fragmented thoughts into full, precise explanations.
- Where appropriate, rephrase or clarify confusing passages for better understanding.
4. Accuracy:
- Preserve the lecturer’s original meaning, intent, and terminology.
- Avoid adding personal opinions or new information that was not in the lecture.
5. Style:
- Write in a formal, academic tone suitable for study notes.
- Aim for readability: concise sentences, but thorough coverage of concepts.
- Use emphasis (e.g., bold or italic text) only when it improves comprehension.
Final Output: A cohesive, detailed, and well-structured lecture summary, suitable for later studying and revision.
Use only russian language!
USE LATEX IN DOLLAR SIGN ($)!
EXTRA BIG LENTH OF CONSPECT!
MAKE AS LONG AS POSIBLE AND AS BE GOOD!
'''

View File

@@ -1,36 +1,36 @@
import re import re
from pylatexenc.latex2text import LatexNodes2Text from pylatexenc.latex2text import LatexNodes2Text
from markdown_pdf import MarkdownPdf from markdown_pdf import MarkdownPdf
from markdown_pdf import Section from markdown_pdf import Section
class ConvertMdToPdf: class ConvertMdToPdf:
# Конвертирует md в pdf # Конвертирует md в pdf
def convertLatexToText(self, text:str): def convertLatexToText(self, text:str):
''' '''
Функция для конвертации LaTeX в текст; Функция для конвертации LaTeX в текст;
Args: Args:
:param text: текст содержащий LaTeX. :param text: текст содержащий LaTeX.
''' '''
# Обрабатываем только математические выражения # Обрабатываем только математические выражения
text = re.sub( text = re.sub(
r'\$\$(.*?)\$\$|\$(.*?)\$', r'\$\$(.*?)\$\$|\$(.*?)\$',
self.replace_math, self.replace_math,
text, text,
flags=re.DOTALL flags=re.DOTALL
) )
pdf = MarkdownPdf(toc_level=0, optimize=True) pdf = MarkdownPdf(toc_level=0, optimize=True)
pdf.add_section(Section(text)) pdf.add_section(Section(text))
return pdf, text return pdf, text
def replace_math(self, match): def replace_math(self, match):
math_content = match.group(1) or match.group(2) # $$...$$ или $...$ math_content = match.group(1) or match.group(2) # $$...$$ или $...$
try: try:
# Преобразуем только математическое выражение # Преобразуем только математическое выражение
converted = LatexNodes2Text().latex_to_text(math_content) converted = LatexNodes2Text().latex_to_text(math_content)
return converted return converted
except: except:
return math_content # В случае ошибки оставляем как есть return math_content # В случае ошибки оставляем как есть

View File

@@ -1,31 +1,31 @@
from pathlib import Path from pathlib import Path
# Для аннотации типов # Для аннотации типов
from markdown_pdf import MarkdownPdf from markdown_pdf import MarkdownPdf
from pydub import AudioSegment from pydub import AudioSegment
class FileHandlers: class FileHandlers:
# Функция сохранения файла # Функция сохранения файла
def saveFile(self, filename, content, output_path, format='mp3'): def saveFile(self, filename, content, output_path, format='mp3'):
''' '''
Сохраняет текст, pdf из markdown_pdf или склеенный аудиофайл в файл с указанным названием и директорией. Сохраняет текст, pdf из markdown_pdf или склеенный аудиофайл в файл с указанным названием и директорией.
Args: Args:
:param filename: название файла; :param filename: название файла;
:param content: содержание файла; :param content: содержание файла;
:param output_path: выходная диретория файла. :param output_path: выходная диретория файла.
''' '''
# Создание объекта директории # Создание объекта директории
directory = Path(output_path) directory = Path(output_path)
filePath = directory / filename # Добавление пути директории filePath = directory / filename # Добавление пути директории
filePath.parent.mkdir(parents=True, exist_ok=True) # Создание директории если не существует filePath.parent.mkdir(parents=True, exist_ok=True) # Создание директории если не существует
# Сохранение для разных типов # Сохранение для разных типов
if type(content) == MarkdownPdf: if type(content) == MarkdownPdf:
return content.save(filePath) return content.save(filePath)
elif type(content) == str: elif type(content) == str:
return filePath.write_text(content, encoding='utf-8') return filePath.write_text(content, encoding='utf-8')
elif type(content) == AudioSegment: elif type(content) == AudioSegment:
return content.export(filePath, format=format) return content.export(filePath, format=format)

View File

@@ -1,11 +1,11 @@
from pydub import AudioSegment from pydub import AudioSegment
class GlueAudio(): class GlueAudio():
def glue(self, audioFiles): def glue(self, audioFiles):
glued = AudioSegment.empty() glued = AudioSegment.empty()
for audioFile in audioFiles: for audioFile in audioFiles:
audio = AudioSegment.from_file(audioFile) audio = AudioSegment.from_file(audioFile)
glued += audio glued += audio
return glued return glued

View File

@@ -1,62 +1,67 @@
class GradioHandlers: from config import LLM_MODELS # Импортируем словарь моделей
def __init__(self, gr, Llm, ConvertMdToPdf, FileHandlers, FasterWhisper, GlueAudio): import gradio as gr
# Объект для работы с файлами
self.fh = FileHandlers()
self.ga = GlueAudio() class GradioHandlers:
self.ConvertMdToPdf = ConvertMdToPdf() def __init__(self, llm_factory, ConvertMdToPdf, FileHandlers, FasterWhisper, GlueAudio):
self.FasterWhisper = FasterWhisper() # Объект для работы с файлами
self.Llm = Llm self.fh = FileHandlers()
self.gr = gr self.ga = GlueAudio()
self.ConvertMdToPdf = ConvertMdToPdf()
def handleRecognizeBtn(self, audioFiles, model, device, compute_type, beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText, filename, outPath): self.FasterWhisper = FasterWhisper()
audioFile = self.ga.glue(audioFiles) self.llm_factory = llm_factory # Сохраняем фабрику
file = self.fh.saveFile(filename, audioFile, outPath)
def handleRecognizeBtn(self, audioFiles, model, device, compute_type, beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText, filename, outPath):
return self.FasterWhisper.recognize(model, device, compute_type, file, beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText) audioFile = self.ga.glue(audioFiles)
file = self.fh.saveFile(filename, audioFile, outPath)
# Функция улучшения текста
def generateByCondition(self, api_key, llm_model, system_prompt, recognized_text, llm_temperature, is_pipeline_enabled, trigger, isSaveFile, filename, filenamePdf, output_path): return self.FasterWhisper.recognize(model, device, compute_type, file, beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText)
llm = self.Llm(api_key)
# Функция улучшения текста
# если чекбокс включен и событие было change → обрабатываем def generateByCondition(self, api_key, llm_provider, llm_model, system_prompt, recognized_text, llm_temperature, is_pipeline_enabled, trigger, isSaveFile, filename, filenamePdf, output_path):
if is_pipeline_enabled and trigger == "change": try:
result, md = llm.generate(llm_model, system_prompt, recognized_text, llm_temperature) # Получаем нужный провайдер через фабрику
provider = self.llm_factory(llm_provider, api_key)
# Конвертируем текст с латексом в юникод except ValueError as e:
pdf, unicodeText = self.ConvertMdToPdf.convertLatexToText(md) # Если API ключ не предоставлен для нужного провайдера, выводим ошибку
self.gr.Warning(str(e))
if isSaveFile: return self.gr.skip(), self.gr.skip()
self.fh.saveFile(filenamePdf, pdf, output_path)
self.fh.saveFile(filename, result, output_path) def process():
result, md = provider.generate(llm_model, system_prompt, recognized_text, llm_temperature)
return result, unicodeText pdf, unicodeText = self.ConvertMdToPdf.convertLatexToText(md)
if isSaveFile:
# если чекбокс выключен и событие было click → обрабатываем self.fh.saveFile(filenamePdf, pdf, output_path)
if not is_pipeline_enabled and trigger == "click": self.fh.saveFile(filename, result, output_path)
result, md = llm.generate(llm_model, system_prompt, recognized_text, llm_temperature) return result, unicodeText
# Конвертируем текст с латексом в юникод if (is_pipeline_enabled and trigger == "change") or (not is_pipeline_enabled and trigger == "click"):
pdf, unicodeText = self.ConvertMdToPdf.convertLatexToText(md) return process()
if isSaveFile: return gr.skip(), gr.skip()
self.fh.saveFile(filenamePdf, pdf, output_path)
self.fh.saveFile(filename, result, output_path) # НОВАЯ ФУНКЦИЯ для обновления списка моделей
def update_model_dropdown(self, provider):
return result, unicodeText """
Вызывается при изменении llmProvider.
# если нет чекбокса и было событие change Возвращает обновленный компонент Dropdown для моделей.
return self.gr.skip(), self.gr.skip() """
# Получаем список моделей для выбранного провайдера
# Функция для динамического обновления кнопки в зависимости от состояния checkbox models = LLM_MODELS.get(provider, [])
def updateButton(self, isChecked):
if not isChecked: # Выбираем первое значение по умолчанию, если список не пуст
variant = 'primary' default_value = models[0] if models else None
else:
variant = 'secondary' # Возвращаем обновленный компонент. Используем 'gr' напрямую.
return gr.update(choices=models, value=default_value)
return self.gr.update(interactive=not isChecked, variant=variant)
# Функция для динамического обновления кнопки
def updateButton(self, isChecked):
def updateTextbox(self, isChecked): if not isChecked:
return self.gr.update(visible=isChecked) variant = 'primary'
else:
variant = 'secondary'
return gr.update(interactive=not isChecked, variant=variant)
def updateTextbox(self, isChecked):
return gr.update(visible=isChecked)

View File

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

View File

@@ -1,34 +1,34 @@
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( # Распознаем текст
audioFile, audioFile,
beam_size=beamSize, beam_size=beamSize,
vad_filter=vadFilter, vad_filter=vadFilter,
vad_parameters={ vad_parameters={
"min_silence_duration_ms": minSilenceDurationMs, "min_silence_duration_ms": minSilenceDurationMs,
"speech_pad_ms": speechPadMs "speech_pad_ms": speechPadMs
}, },
temperature= [temp0, temp1, temp2], temperature= [temp0, temp1, temp2],
word_timestamps=wordTimestamps, word_timestamps=wordTimestamps,
no_speech_threshold=noSpeechThreshold, no_speech_threshold=noSpeechThreshold,
condition_on_previous_text=conditionOnPreviousText condition_on_previous_text=conditionOnPreviousText
) )
text = '' text = ''
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'
return text return text
def format_timestamp(self, seconds: float) -> str: def format_timestamp(self, seconds: float) -> str:
millis = int(seconds * 1000) millis = int(seconds * 1000)
hours = millis // (3600 * 1000) hours = millis // (3600 * 1000)
minutes = (millis % (3600 * 1000)) // (60 * 1000) minutes = (millis % (3600 * 1000)) // (60 * 1000)
seconds_int = (millis % (60 * 1000)) // 1000 seconds_int = (millis % (60 * 1000)) // 1000
millis = millis % 1000 millis = millis % 1000
return f"{hours:02d}:{minutes:02d}:{seconds_int:02d},{millis:03d}" return f"{hours:02d}:{minutes:02d}:{seconds_int:02d},{millis:03d}"

View File

@@ -1,36 +0,0 @@
import openai
class Llm:
def __init__(self, apiKey:str):
self.client = openai.OpenAI(
api_key=apiKey,
base_url='https://api.intelligence.io.solutions/api/v1/'
)
def generate(self, model:str, systemPrompt:str, userPrompt:str, temp:float):
'''
Функция для генирации текста по промпту.
Args:
:param model: модель llm;
:param systemPrompt: системный промпт;
:param userPrompt: основной промпт промпт;
:param temp: температура генерации.
'''
# Получаем ответ от нейросети
response = self.client.chat.completions.create(
model=model,
messages=[
{'role': 'system', 'content': systemPrompt},
{'role': 'user', 'content': userPrompt},
],
temperature=temp,
stream=False
)
# Достаем текст
text = str(response.choices[0].message.content)
return text, text

22
services/llm_factory.py Normal file
View File

@@ -0,0 +1,22 @@
# services/llm_factory.py
from services.llm_providers.ionet_provider import IoNetProvider
from services.llm_providers.gemini_provider import GeminiProvider
from services.llm_providers.gpt4free_provider import Gpt4FreeProvider
from services.llm_providers.base_provider import BaseLLMProvider
def get_llm_provider(provider_name: str, api_key: str | None) -> BaseLLMProvider:
"""
Фабричная функция для получения экземпляра провайдера LLM.
"""
if provider_name == 'io.net':
if not api_key:
raise ValueError("API ключ обязателен для io.net")
return IoNetProvider(api_key)
elif provider_name == 'Gemini':
if not api_key:
raise ValueError("API ключ обязателен для Gemini")
return GeminiProvider(api_key)
elif provider_name == 'gpt4free':
return Gpt4FreeProvider()
else:
raise ValueError(f"Неизвестный провайдер: {provider_name}")

View File

@@ -0,0 +1,18 @@
from abc import ABC, abstractmethod
class BaseLLMProvider(ABC):
"""
Абстрактный базовый класс для всех провайдеров LLM.
Каждый провайдер должен реализовать метод generate.
"""
def __init__(self, api_key: str | None = None):
self.api_key = api_key
@abstractmethod
def generate(self, model: str, system_prompt: str, user_prompt: str, temp: float):
"""
Основной метод для генерации текста.
Должен возвращать кортеж из двух строк: (чистый_текст, markdown_текст)
"""
pass

View File

@@ -0,0 +1,74 @@
# services/llm_providers/gemini_provider.py
import requests
from .base_provider import BaseLLMProvider
class GeminiProvider(BaseLLMProvider):
"""
Провайдер для Google Gemini, использующий прямые REST API вызовы
через библиотеку requests для надежной работы с SOCKS-прокси.
"""
def __init__(self, api_key: str):
super().__init__(api_key)
self.base_url = "https://generativelanguage.googleapis.com/v1beta/models/"
def generate(self, model: str, system_prompt: str, user_prompt: str, temp: float):
"""
Генерирует текст с помощью модели Gemini, отправляя запрос через прокси.
"""
# 1. Формируем URL для запроса
api_url = f"{self.base_url}{model}:generateContent?key={self.api_key}"
# 2. Задаем настройки прокси из вашего примера
# socks5h:// означает, что DNS-запросы также будут идти через прокси
proxies = {
'http': 'socks5://192.168.1.6:2080',
'https': 'socks5h://192.168.1.6:2080'
}
# 3. Собираем тело запроса (payload) в формате, который ожидает Gemini API
data = {
"system_instruction": {
"parts": {"text": system_prompt}
},
"contents": [{
"parts": [{"text": user_prompt}]
}],
"generationConfig": {
"temperature": temp
}
}
try:
# 4. Отправляем POST-запрос с данными и настройками прокси
response = requests.post(api_url, json=data, proxies=proxies, timeout=90)
# Проверяем, не вернул ли сервер ошибку (например, 4xx или 5xx)
response.raise_for_status()
# 5. Парсим JSON-ответ и извлекаем сгенерированный текст
response_json = response.json()
# Добавим проверку на случай, если контент был заблокирован
if "candidates" not in response_json or not response_json["candidates"]:
block_reason = response_json.get("promptFeedback", {}).get("blockReason", "неизвестная причина")
error_message = f"Контент заблокирован. Причина: {block_reason}"
return error_message, error_message
text = response_json["candidates"][0]["content"]["parts"][0]["text"]
return text, text
except requests.exceptions.ProxyError as e:
error_message = f"Ошибка подключения к прокси. Убедитесь, что Nekobox запущен и слушает порт 2080. Ошибка: {e}"
print(error_message)
return error_message, error_message
except requests.exceptions.RequestException as e:
# Ловим все остальные ошибки requests (таймаут, проблемы с сетью и т.д.)
error_message = f"Произошла ошибка при обращении к API Gemini: {e}"
print(error_message)
return error_message, error_message
except (KeyError, IndexError) as e:
# Ловим ошибки, если структура JSON-ответа неожиданная
error_message = f"Не удалось разобрать ответ от API Gemini. Структура ответа изменилась. Ошибка: {e}"
print(error_message)
return error_message, error_message

View File

@@ -0,0 +1,28 @@
# services/llm_providers/gpt4free_provider.py
from g4f.client import Client
from .base_provider import BaseLLMProvider
class Gpt4FreeProvider(BaseLLMProvider):
# gpt4free не требует API ключа
def __init__(self, api_key: str | None = None):
super().__init__(api_key)
self.client = Client()
def generate(self, model: str, system_prompt: str, user_prompt: str, temp: float):
# temp в g4f может работать не для всех внутренних провайдеров
try:
response = self.client.chat.completions.create(
model=model, # Пример модели, может варьироваться в зависимости от доступности провайдеров
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=temp
)
text = response.choices[0].message.content
return text, text
except Exception as e:
error_message = f"Ошибка при работе с gpt4free: {e}"
print(error_message)
return error_message, error_message

View File

@@ -0,0 +1,23 @@
import openai
from .base_provider import BaseLLMProvider
class IoNetProvider(BaseLLMProvider):
def __init__(self, api_key: str):
super().__init__(api_key)
self.client = openai.OpenAI(
api_key=self.api_key,
base_url='https://api.intelligence.io.solutions/api/v1/'
)
def generate(self, model: str, system_prompt: str, user_prompt: str, temp: float):
response = self.client.chat.completions.create(
model=model,
messages=[
{'role': 'system', 'content': system_prompt},
{'role': 'user', 'content': user_prompt},
],
temperature=temp,
stream=False
)
text = str(response.choices[0].message.content)
return text, text # Возвращаем как чистый текст, так и Markdown