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

1
.gitignore vendored
View File

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

38
app.py
View File

@@ -4,7 +4,9 @@ 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.gradioHandler import GradioHandlers
@@ -13,7 +15,7 @@ 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
gh = GradioHandlers(gr, Llm, ConvertMdToPdf, FileHandlers, FasterWhisper, GlueAudio) gh = GradioHandlers(get_llm_provider, ConvertMdToPdf, FileHandlers, FasterWhisper, GlueAudio)
def main(): def main():
with gr.Blocks() as demo: with gr.Blocks() as demo:
@@ -33,7 +35,7 @@ def main():
with gr.Row(): with gr.Row():
with gr.Accordion(label='Recognization and integration'): with gr.Accordion(label='Recognization and integration'):
with gr.Column(): with gr.Column():
audioFiles = gr.Files(label='Load audio for transcribe', type="filepath", file_types=['audio']) audioFiles = gr.Files(label='Load audio for transcribe', type="filepath")
images = gr.Files(label='Upload images', file_types=['image']) images = gr.Files(label='Upload images', file_types=['image'])
recognizeBtn = gr.Button('recognize and integrate', variant='primary') recognizeBtn = gr.Button('recognize and integrate', variant='primary')
@@ -86,14 +88,27 @@ def main():
temp2 = gr.Number(label='temp_2', value=0.4, interactive=True) temp2 = gr.Number(label='temp_2', value=0.4, interactive=True)
# Нижний акордион настроек для api ключа llm # Нижний акордион настроек для api ключа llm
with gr.Accordion(label='ai.io.net api settings'): with gr.Accordion(label='LLM settings'):
apiKey = gr.Textbox(label='API key', value=DEFAULT_API_KEY, interactive=True) apiKey = gr.Textbox(label='API key (required for io.net, Gemini)', value=DEFAULT_API_KEY, interactive=True)
with gr.Accordion(label='System prompt'): with gr.Accordion(label='System prompt'):
systemPrompt = gr.Textbox(label='', value=DEFAULT_SYSTEM_PROMPT, interactive=True) systemPrompt = gr.Textbox(label='', value=DEFAULT_SYSTEM_PROMPT, interactive=True)
with gr.Row(): with gr.Row():
llmModel = gr.Dropdown(label='models', choices=LLM_MODELS, value=LLM_MODELS[1], interactive=True) # ВЫБОР ПРОВАЙДЕРА
llmProvider = gr.Dropdown(
label='LLM Provider',
choices=LLM_PROVIDERS,
value=LLM_PROVIDERS[0],
interactive=True
)
# СПИСОК МОДЕЛЕЙ (теперь зависит от провайдера)
llmModel = gr.Dropdown(
label='Models',
choices=LLM_MODELS[LLM_PROVIDERS[0]], # Модели для провайдера по умолчанию
value=LLM_MODELS[LLM_PROVIDERS[0]][1],
interactive=True
)
llmTemperature = gr.Number(label='Temperature', value=0.8, interactive=True) llmTemperature = gr.Number(label='Temperature', value=0.8, interactive=True)
isPipelineEnabledCheckbox.change(gh.updateButton, inputs=[isPipelineEnabledCheckbox], outputs=refineTextBtn) isPipelineEnabledCheckbox.change(gh.updateButton, inputs=[isPipelineEnabledCheckbox], outputs=refineTextBtn)
@@ -106,14 +121,21 @@ def main():
# автоматический пайплайн # автоматический пайплайн
recognizedText.change( recognizedText.change(
gh.generateByCondition, gh.generateByCondition,
inputs=[apiKey, 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]
) )
# ручной запуск по кнопке # ручной запуск по кнопке
llmProvider.change(
gh.update_model_dropdown,
inputs=llmProvider,
outputs=llmModel
)
refineTextBtn.click( refineTextBtn.click(
gh.generateByCondition, gh.generateByCondition,
inputs=[apiKey, 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

@@ -4,12 +4,43 @@ 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')
# Словарь провайдеров и их моделей
LLM_PROVIDERS = ['io.net', 'Gemini', 'gpt4free']
LLM_MODELS = {
'io.net': [
'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'
],
'Gemini': [
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite'
],
'gpt4free': [ # Модели могут меняться, проверьте документацию g4f
'default',
'gpt-4',
'sonar-reasoning',
'command-r-plus',
'llama-3.3-70b',
'hermes-3-llama-3.1-405b'
'qwen-3-235b',
'gpt-4o-mini',
'deepseek-r1',
'PollinationsAI:gpt-5-nano'
]
}
# Задаем выходную директорию # Задаем выходную директорию
OUTPUT_PATH='outputs' OUTPUT_PATH='outputs'

View File

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

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