Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd01abff8b | ||
|
|
9272ce4123 | ||
|
|
8ba0f89f89 |
79
app.py
79
app.py
@@ -1,7 +1,13 @@
|
|||||||
import gradio as gr
|
import gradio as gr
|
||||||
|
|
||||||
|
# Загрузка параметров конфигурации
|
||||||
from config import *
|
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 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 handlers.convertMdToPdf import ConvertMdToPdf
|
from handlers.convertMdToPdf import ConvertMdToPdf
|
||||||
@@ -11,9 +17,16 @@ gh = GradioHandlers(get_llm_provider, ConvertMdToPdf, FileHandlers, FasterWhispe
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
with gr.Blocks() as demo:
|
with gr.Blocks() as demo:
|
||||||
gr.HTML('<div align=center><h1>Faster Whisper WebUI</h1></div>')
|
gr.HTML('''
|
||||||
|
<div align=center>
|
||||||
|
<h1>
|
||||||
|
Faster Whisper WebUI
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
''')
|
||||||
|
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
|
# Вкладка с основным взаимодействием
|
||||||
with gr.Tab('Actions'):
|
with gr.Tab('Actions'):
|
||||||
isPipelineEnabledCheckbox = gr.Checkbox(label='is pipeline enabled', value=True, interactive=True)
|
isPipelineEnabledCheckbox = gr.Checkbox(label='is pipeline enabled', value=True, interactive=True)
|
||||||
|
|
||||||
@@ -23,64 +36,79 @@ def main():
|
|||||||
audioFiles = gr.Files(label='Load audio for transcribe', type="filepath")
|
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')
|
||||||
|
|
||||||
with gr.Accordion(label='Recognized text'):
|
with gr.Accordion(label='Recognized text'):
|
||||||
recognizedText = gr.TextArea(label='')
|
recognizedText = gr.TextArea(label='')
|
||||||
|
|
||||||
with gr.Accordion(label='LLM'):
|
with gr.Accordion(label='LLM'):
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
refineTextBtn = gr.Button('refine text', variant='secondary', interactive=False)
|
refineTextBtn = gr.Button('refine text', variant='secondary', interactive=False)
|
||||||
|
|
||||||
with gr.Accordion(label='Refined text raw'):
|
with gr.Accordion(label='Refined text raw'):
|
||||||
refinedText = gr.Textbox(label='', show_copy_button=True)
|
refinedText = gr.Textbox(label='', show_copy_button=True)
|
||||||
|
|
||||||
with gr.Accordion(label='Refined text md formated'):
|
with gr.Accordion(label='Refined text md formated'):
|
||||||
refinedTextMD = gr.Markdown(label='')
|
refinedTextMD = gr.Markdown(label='')
|
||||||
|
|
||||||
|
# Вкладка с настройками
|
||||||
with gr.Tab('Settings'):
|
with gr.Tab('Settings'):
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
|
# Первое поле на всю ширину в акордионе настроек
|
||||||
with gr.Accordion('File settings'):
|
with gr.Accordion('File settings'):
|
||||||
saveFileCheckbox = gr.Checkbox(label='save file', value=True, interactive=True)
|
saveFileCheckbox = gr.Checkbox(label='save file', value=True, interactive=True)
|
||||||
filename = gr.Textbox(label='Output filename', value='output.md', interactive=True)
|
filename = gr.Textbox(label='Output filename', value='output.txt', interactive=True)
|
||||||
filenamePdf = gr.Textbox(label='Output filename for pdf', value='output.pdf', interactive=True)
|
filenamePdf = gr.Textbox(label='Output filename for pdf', value='output.pdf', interactive=True)
|
||||||
|
|
||||||
|
# Акордион настроек faster whisper
|
||||||
with gr.Accordion(label='Faster whisper settings'):
|
with gr.Accordion(label='Faster whisper settings'):
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
|
# Левая колонка в акордионе
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
device = gr.Dropdown(label='Device', choices=DEVICES, value=DEVICES[1], 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)
|
compute_type = gr.Dropdown(label='compute_type', choices=COMPUTE_TYPE, value=COMPUTE_TYPE[0], interactive=True)
|
||||||
fastWhisperModel = gr.Dropdown(label='Model', choices=FAST_WHISPER_MODELS, value=FAST_WHISPER_MODELS[11], interactive=True)
|
fastWhisperModel = gr.Dropdown(label='Model', choices=FAST_WHISPER_MODELS, value=FAST_WHISPER_MODELS[11], interactive=True)
|
||||||
|
|
||||||
beamSize = gr.Number(label='beam_size', value=8, interactive=True)
|
beamSize = gr.Number(label='beam_size', value=8, interactive=True)
|
||||||
noSpeechThreshold = gr.Number(label='no_speech_threshold', value=0.5, interactive=True)
|
noSpeechThreshold = gr.Number(label='no_speech_threshold', value=0.5, interactive=True)
|
||||||
vadFilter = gr.Checkbox(label='vad_filter', value=True, interactive=True)
|
vadFilter = gr.Checkbox(label='vad_filter', value=True, interactive=True)
|
||||||
wordTimestamps = gr.Checkbox(label='word_timestamps', 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)
|
conditionOnPreviousText = gr.Checkbox(label='condition_on_previous_text', value=False, interactive=True)
|
||||||
|
|
||||||
|
# Правая колонка в акордионе
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
with gr.Accordion(label='Vad parameters'):
|
with gr.Accordion(label='Vad parameters'):
|
||||||
minSilenceDurationMs = gr.Number(label='min_silence_duration_ms', value=300, interactive=True)
|
minSilenceDurationMs = gr.Number(label='min_silence_duration_ms', value=300, interactive=True)
|
||||||
speechPadMs = gr.Number(label='speech_pad_ms', value=200, interactive=True)
|
speechPadMs = gr.Number(label='speech_pad_ms', value=200, interactive=True)
|
||||||
|
|
||||||
with gr.Accordion(label='Temperature'):
|
with gr.Accordion(label='Temperature'):
|
||||||
temp0 = gr.Number(label='temp_0', value=0.0, interactive=True)
|
temp0 = gr.Number(label='temp_0', value=0.0, interactive=True)
|
||||||
temp1 = gr.Number(label='temp_1', value=0.2, interactive=True)
|
temp1 = gr.Number(label='temp_1', value=0.2, interactive=True)
|
||||||
temp2 = gr.Number(label='temp_2', value=0.4, interactive=True)
|
temp2 = gr.Number(label='temp_2', value=0.4, interactive=True)
|
||||||
|
|
||||||
|
# Нижний акордион настроек для api ключа llm
|
||||||
with gr.Accordion(label='LLM settings'):
|
with gr.Accordion(label='LLM settings'):
|
||||||
apiKey = gr.Textbox(label='API key (required for io.net, Gemini)', 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():
|
||||||
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)
|
llmProvider = gr.Dropdown(
|
||||||
llmTemperature = gr.Number(label='Temperature', value=0.8, interactive=True)
|
label='LLM Provider',
|
||||||
|
choices=LLM_PROVIDERS,
|
||||||
# Настройки Custom провайдера
|
value=LLM_PROVIDERS[0],
|
||||||
with gr.Accordion(label='Custom Provider Settings', open=True):
|
interactive=True
|
||||||
customBaseUrl = gr.Textbox(
|
|
||||||
label='Base URL',
|
|
||||||
value='http://127.0.0.1:1234/v1/',
|
|
||||||
interactive=True,
|
|
||||||
visible=False # Скрыто по умолчанию
|
|
||||||
)
|
)
|
||||||
|
# СПИСОК МОДЕЛЕЙ (теперь зависит от провайдера)
|
||||||
|
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)
|
||||||
|
|
||||||
# Обработчики событий
|
|
||||||
isPipelineEnabledCheckbox.change(gh.updateButton, inputs=[isPipelineEnabledCheckbox], outputs=refineTextBtn)
|
isPipelineEnabledCheckbox.change(gh.updateButton, inputs=[isPipelineEnabledCheckbox], outputs=refineTextBtn)
|
||||||
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)
|
||||||
@@ -91,38 +119,33 @@ def main():
|
|||||||
vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2,
|
vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2,
|
||||||
wordTimestamps, noSpeechThreshold, conditionOnPreviousText, gr.State(GLUED_AUDIO_FILENAME), gr.State(OUTPUT_PATH)],
|
wordTimestamps, noSpeechThreshold, conditionOnPreviousText, gr.State(GLUED_AUDIO_FILENAME), gr.State(OUTPUT_PATH)],
|
||||||
outputs=[recognizedText],
|
outputs=[recognizedText],
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- ИСПРАВЛЕНИЕ: ДОБАВЛЕН customBaseUrl В INPUTS ---
|
# Если пайплайн включен то тогда делаем автоматически
|
||||||
|
# автоматический пайплайн
|
||||||
recognizedText.change(
|
recognizedText.change(
|
||||||
gh.generateByCondition,
|
gh.generateByCondition,
|
||||||
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature,
|
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature,
|
||||||
isPipelineEnabledCheckbox, gr.State("change"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH), customBaseUrl],
|
isPipelineEnabledCheckbox, gr.State("change"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)],
|
||||||
outputs=[refinedText, refinedTextMD]
|
outputs=[refinedText, refinedTextMD]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Обновление выпадающего списка моделей и поля API ключа
|
|
||||||
|
# ручной запуск по кнопке
|
||||||
llmProvider.change(
|
llmProvider.change(
|
||||||
gh.update_model_dropdown,
|
gh.update_model_dropdown,
|
||||||
inputs=llmProvider,
|
inputs=llmProvider,
|
||||||
outputs=[llmModel, apiKey]
|
outputs=[llmModel, apiKey]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Переключение видимости URL для Custom провайдера
|
|
||||||
llmProvider.change(
|
|
||||||
fn=gh.toggle_custom_url,
|
|
||||||
inputs=llmProvider,
|
|
||||||
outputs=[customBaseUrl]
|
|
||||||
)
|
|
||||||
|
|
||||||
refineTextBtn.click(
|
refineTextBtn.click(
|
||||||
gh.generateByCondition,
|
gh.generateByCondition,
|
||||||
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature,
|
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature,
|
||||||
isPipelineEnabledCheckbox, gr.State("click"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH), customBaseUrl],
|
isPipelineEnabledCheckbox, gr.State("click"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)],
|
||||||
outputs=[refinedText, refinedTextMD]
|
outputs=[refinedText, refinedTextMD]
|
||||||
)
|
)
|
||||||
|
|
||||||
demo.launch()
|
demo.launch()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
13
config.py
13
config.py
@@ -14,7 +14,7 @@ DEFAULT_API_KEY=IO_API_KEY
|
|||||||
|
|
||||||
|
|
||||||
# Словарь провайдеров и их моделей
|
# Словарь провайдеров и их моделей
|
||||||
LLM_PROVIDERS = ['io.net', 'Gemini', 'gpt4free', 'Custom']
|
LLM_PROVIDERS = ['io.net', 'Gemini', 'gpt4free']
|
||||||
LLM_MODELS = {
|
LLM_MODELS = {
|
||||||
'io.net': [
|
'io.net': [
|
||||||
'openai/gpt-oss-120b', 'Qwen/Qwen3-235B-A22B-Thinking-2507',
|
'openai/gpt-oss-120b', 'Qwen/Qwen3-235B-A22B-Thinking-2507',
|
||||||
@@ -30,7 +30,7 @@ LLM_MODELS = {
|
|||||||
'gemini-2.5-flash',
|
'gemini-2.5-flash',
|
||||||
'gemini-2.5-flash-lite'
|
'gemini-2.5-flash-lite'
|
||||||
],
|
],
|
||||||
'gpt4free': [
|
'gpt4free': [ # Модели могут меняться, проверьте документацию g4f
|
||||||
'default',
|
'default',
|
||||||
'gpt-4',
|
'gpt-4',
|
||||||
'sonar-reasoning',
|
'sonar-reasoning',
|
||||||
@@ -41,14 +41,7 @@ LLM_MODELS = {
|
|||||||
'gpt-4o-mini',
|
'gpt-4o-mini',
|
||||||
'deepseek-r1',
|
'deepseek-r1',
|
||||||
'PollinationsAI:gpt-5-nano'
|
'PollinationsAI:gpt-5-nano'
|
||||||
],
|
]
|
||||||
'Custom': [
|
|
||||||
'qwen/qwen3-vl-30b',
|
|
||||||
'qwen/qwen3-coder-30b',
|
|
||||||
'openai/gpt-oss-20b',
|
|
||||||
'qwen3-vl-8b-thinking',
|
|
||||||
'qwen/qwen3-vl-8b',
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Задаем выходную директорию
|
# Задаем выходную директорию
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from pathlib import Path
|
|||||||
class GlueAudio():
|
class GlueAudio():
|
||||||
def glue(self, audio_files: list, output_path: str, output_filename: str) -> Path:
|
def glue(self, audio_files: list, output_path: str, output_filename: str) -> Path:
|
||||||
"""
|
"""
|
||||||
Склеивает аудиофайлы РАЗНЫХ форматов с помощью FFmpeg и filter_complex.
|
Склеивает аудиофайлы с помощью FFmpeg, используя промежуточный список файлов.
|
||||||
Это универсальный и эффективный по памяти метод.
|
Этот метод чрезвычайно эффективен по памяти и скорости.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
audio_files (list): Список путей к исходным аудиофайлам.
|
audio_files (list): Список путей к исходным аудиофайлам.
|
||||||
@@ -58,4 +58,4 @@ class GlueAudio():
|
|||||||
print("Stderr:", e.stderr)
|
print("Stderr:", e.stderr)
|
||||||
raise RuntimeError(f"Ошибка FFmpeg при склейке файлов: {e.stderr}")
|
raise RuntimeError(f"Ошибка FFmpeg при склейке файлов: {e.stderr}")
|
||||||
|
|
||||||
return final_audio_path
|
return final_audio_path
|
||||||
|
|||||||
@@ -1,52 +1,50 @@
|
|||||||
from config import LLM_MODELS, GEMINI_API_KEY, IO_API_KEY
|
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):
|
||||||
|
# Объект для работы с файлами
|
||||||
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_factory = llm_factory
|
self.llm_factory = llm_factory
|
||||||
|
|
||||||
def handleRecognizeBtn(self, audioFiles, model, device, compute_type, beamSize, vadFilter,
|
def handleRecognizeBtn(
|
||||||
|
self, audioFiles, model, device, compute_type, beamSize, vadFilter,
|
||||||
minSilenceDurationMs, speechPadMs, temp0, temp1, temp2,
|
minSilenceDurationMs, speechPadMs, temp0, temp1, temp2,
|
||||||
wordTimestamps, noSpeechThreshold, conditionOnPreviousText, filename, outPath):
|
wordTimestamps, noSpeechThreshold, conditionOnPreviousText, filename, outPath
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
glued_audio_path = self.ga.glue(
|
glued_audio_path = self.ga.glue(
|
||||||
audio_files=[f.name for f in audioFiles],
|
audio_files=[f.name for f in audioFiles], # Передаем список путей
|
||||||
output_path=outPath,
|
output_path=outPath,
|
||||||
output_filename=filename
|
output_filename=filename
|
||||||
)
|
)
|
||||||
except (FileNotFoundError, RuntimeError) as e:
|
except (FileNotFoundError, RuntimeError) as e:
|
||||||
|
# Если FFmpeg не найден или произошла ошибка, сообщаем пользователю
|
||||||
gr.Warning(str(e))
|
gr.Warning(str(e))
|
||||||
return ""
|
return "" # Возвращаем пустую строку в текстовое поле
|
||||||
|
|
||||||
|
# Передаем путь к склеенному файлу в FasterWhisper
|
||||||
return self.FasterWhisper.recognize(model, device, compute_type, str(glued_audio_path), beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText)
|
return self.FasterWhisper.recognize(model, device, compute_type, str(glued_audio_path), beamSize, vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2, wordTimestamps, noSpeechThreshold, conditionOnPreviousText)
|
||||||
|
|
||||||
# Добавил аргумент custom_base_url в конец
|
# Функция улучшения текста
|
||||||
def generateByCondition(self, api_key, llm_provider,
|
def generateByCondition(self, api_key, llm_provider,
|
||||||
llm_model, system_prompt, recognized_text,
|
llm_model, system_prompt, recognized_text,
|
||||||
llm_temperature, is_pipeline_enabled, trigger,
|
llm_temperature, is_pipeline_enabled, trigger,
|
||||||
isSaveFile, filename, filenamePdf, output_path, custom_base_url):
|
isSaveFile, filename, filenamePdf, output_path):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if llm_provider == "Custom":
|
# Получаем нужный провайдер через фабрику
|
||||||
# Передаем base_url только для Custom
|
provider = self.llm_factory(llm_provider, api_key)
|
||||||
provider = self.llm_factory(llm_provider, api_key, base_url=custom_base_url)
|
|
||||||
else:
|
|
||||||
provider = self.llm_factory(llm_provider, api_key)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
# Если API ключ не предоставлен для нужного провайдера, выводим ошибку
|
||||||
gr.Warning(str(e))
|
gr.Warning(str(e))
|
||||||
return gr.skip(), gr.skip()
|
return gr.skip(), gr.skip()
|
||||||
|
|
||||||
def process():
|
def process():
|
||||||
# Добавлена обработка ошибок генерации
|
result, md = provider.generate(llm_model, system_prompt, recognized_text, llm_temperature)
|
||||||
try:
|
|
||||||
result, md = provider.generate(llm_model, system_prompt, recognized_text, llm_temperature)
|
|
||||||
except Exception as e:
|
|
||||||
raise gr.Error(f"Ошибка генерации LLM: {e}")
|
|
||||||
|
|
||||||
pdf, unicodeText = self.ConvertMdToPdf.convertLatexToText(md)
|
pdf, unicodeText = self.ConvertMdToPdf.convertLatexToText(md)
|
||||||
if isSaveFile:
|
if isSaveFile:
|
||||||
self.fh.saveFile(filenamePdf, pdf, output_path)
|
self.fh.saveFile(filenamePdf, pdf, output_path)
|
||||||
@@ -58,30 +56,30 @@ class GradioHandlers:
|
|||||||
|
|
||||||
return gr.skip(), gr.skip()
|
return gr.skip(), gr.skip()
|
||||||
|
|
||||||
|
# НОВАЯ ФУНКЦИЯ для обновления списка моделей
|
||||||
def update_model_dropdown(self, provider):
|
def update_model_dropdown(self, provider):
|
||||||
|
"""
|
||||||
|
Вызывается при изменении llmProvider.
|
||||||
|
Возвращает обновленный компонент Dropdown для моделей.
|
||||||
|
"""
|
||||||
|
# Получаем список моделей для выбранного провайдера
|
||||||
models = LLM_MODELS.get(provider, [])
|
models = LLM_MODELS.get(provider, [])
|
||||||
|
|
||||||
|
# Выбираем первое значение по умолчанию, если список не пуст
|
||||||
default_value = models[0] if models else None
|
default_value = models[0] if models else None
|
||||||
|
|
||||||
# Обновляем список моделей и настройки поля API Key
|
# Возвращаем обновленный компонент. Используем 'gr' напрямую.
|
||||||
if provider == 'io.net':
|
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)
|
||||||
return gr.update(choices=models, value=default_value), gr.update(label='API key', value=IO_API_KEY, interactive=True, visible=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 == 'Gemini':
|
if provider == 'gpt4free': return gr.update(choices=models, value=default_value), gr.update(label='API key (required for Oio.net, Gemini)', value="", interactive=True)
|
||||||
return gr.update(choices=models, value=default_value), gr.update(label='API key', value=GEMINI_API_KEY, interactive=True, visible=True)
|
|
||||||
if provider == 'gpt4free':
|
|
||||||
return gr.update(choices=models, value=default_value), gr.update(label='API key (not required)', value="", interactive=False, visible=True)
|
|
||||||
if provider == "Custom":
|
|
||||||
return gr.update(choices=models, value=default_value), gr.update(label='API key (optional)', value="", interactive=True, visible=True) # Для Custom ключ может понадобиться
|
|
||||||
|
|
||||||
|
# Функция для динамического обновления кнопки
|
||||||
def updateButton(self, isChecked):
|
def updateButton(self, isChecked):
|
||||||
variant = 'secondary' if isChecked else 'primary'
|
if not isChecked:
|
||||||
|
variant = 'primary'
|
||||||
|
else:
|
||||||
|
variant = 'secondary'
|
||||||
return gr.update(interactive=not isChecked, variant=variant)
|
return gr.update(interactive=not isChecked, variant=variant)
|
||||||
|
|
||||||
def toggle_custom_url(self, provider):
|
|
||||||
"""Показывает поле Base URL только если выбран Custom"""
|
|
||||||
return gr.update(visible=(provider == 'Custom'))
|
|
||||||
|
|
||||||
def update_custom_url(self, base_url):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def updateTextbox(self, isChecked):
|
def updateTextbox(self, isChecked):
|
||||||
return gr.update(visible=isChecked)
|
return gr.update(visible=isChecked)
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import os
|
|
||||||
from PIL import Image, ExifTags
|
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
class MetadataHandler:
|
|
||||||
def get_image_timestamp(self, image_path: str) -> datetime | None:
|
|
||||||
"""Извлекает метку времени из метаданных изображения, если она доступна."""
|
|
||||||
try:
|
|
||||||
image = Image.open(image_path)
|
|
||||||
exif_data = image._getexif()
|
|
||||||
if exif_data:
|
|
||||||
for tag, value in exif_data.items():
|
|
||||||
decoded_tag = ExifTags.TAGS.get(tag, tag)
|
|
||||||
if decoded_tag == 'DateTimeOriginal':
|
|
||||||
return value
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error extracting metadata from image: {e}")
|
|
||||||
return None
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
pandoc "out.md" -o output1310.pdf \
|
|
||||||
--pdf-engine=xelatex \
|
|
||||||
-V geometry:margin=2.5cm \
|
|
||||||
-V fontsize=12pt \
|
|
||||||
-V mainfont="Times New Roman" \
|
|
||||||
-V colorlinks=true \
|
|
||||||
-V linkcolor=blue\
|
|
||||||
115
requirements.txt
115
requirements.txt
@@ -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
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ from services.llm_providers.ionet_provider import IoNetProvider
|
|||||||
from services.llm_providers.gemini_provider import GeminiProvider
|
from services.llm_providers.gemini_provider import GeminiProvider
|
||||||
from services.llm_providers.gpt4free_provider import Gpt4FreeProvider
|
from services.llm_providers.gpt4free_provider import Gpt4FreeProvider
|
||||||
from services.llm_providers.base_provider import BaseLLMProvider
|
from services.llm_providers.base_provider import BaseLLMProvider
|
||||||
from services.llm_providers.custom_provider import CustomProvider
|
|
||||||
|
|
||||||
def get_llm_provider(provider_name: str, api_key: str | None = None, base_url: str | None = None) -> BaseLLMProvider:
|
def get_llm_provider(provider_name: str, api_key: str | None) -> BaseLLMProvider:
|
||||||
"""
|
"""
|
||||||
Фабричная функция для получения экземпляра провайдера LLM.
|
Фабричная функция для получения экземпляра провайдера LLM.
|
||||||
"""
|
"""
|
||||||
@@ -19,9 +18,5 @@ def get_llm_provider(provider_name: str, api_key: str | None = None, base_url: s
|
|||||||
return GeminiProvider(api_key)
|
return GeminiProvider(api_key)
|
||||||
elif provider_name == 'gpt4free':
|
elif provider_name == 'gpt4free':
|
||||||
return Gpt4FreeProvider()
|
return Gpt4FreeProvider()
|
||||||
elif provider_name == 'Custom':
|
|
||||||
if not base_url:
|
|
||||||
raise ValueError("Base URL обязателен для Custom провайдера")
|
|
||||||
return CustomProvider(api_key, base_url) # base_url будет установлен позже
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Неизвестный провайдер: {provider_name}")
|
raise ValueError(f"Неизвестный провайдер: {provider_name}")
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import requests
|
|
||||||
from .base_provider import BaseLLMProvider
|
|
||||||
|
|
||||||
class CustomProvider(BaseLLMProvider):
|
|
||||||
def __init__(self, api_key: str | None = None, base_url: str | None = None):
|
|
||||||
super().__init__(api_key)
|
|
||||||
self.base_url = base_url or 'http://127.0.0.1:1234/v1/'
|
|
||||||
|
|
||||||
# ГАРАНТИРУЕМ наличие слеша в конце URL
|
|
||||||
if not self.base_url.endswith('/'):
|
|
||||||
self.base_url += '/'
|
|
||||||
|
|
||||||
def generate(self, model: str, system_prompt: str, user_prompt: str, temp: float):
|
|
||||||
url = f"{self.base_url}chat/completions"
|
|
||||||
|
|
||||||
# Некоторые Custom провайдеры (как vLLM или Ollama) могут требовать API Key, даже если он фиктивный
|
|
||||||
headers = {"Content-Type": "application/json"}
|
|
||||||
if self.api_key:
|
|
||||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"model": model,
|
|
||||||
"messages": [
|
|
||||||
{'role': 'system', 'content': system_prompt},
|
|
||||||
{'role': 'user', 'content': user_prompt},
|
|
||||||
],
|
|
||||||
"temperature": temp,
|
|
||||||
"stream": False
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(url, headers=headers, json=data, timeout=300)
|
|
||||||
response.raise_for_status()
|
|
||||||
result = response.json()
|
|
||||||
|
|
||||||
# Обработка разных форматов ответа (на всякий случай)
|
|
||||||
if 'choices' in result and len(result['choices']) > 0:
|
|
||||||
text = str(result['choices'][0]['message']['content'])
|
|
||||||
return text, text
|
|
||||||
else:
|
|
||||||
return f"Неожиданный ответ от сервера: {result}", str(result)
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
return f"Ошибка при запросе к CustomProvider API: {e}", f"Ошибка: {e}"
|
|
||||||
@@ -41,7 +41,7 @@ class GeminiProvider(BaseLLMProvider):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 4. Отправляем POST-запрос с данными и настройками прокси
|
# 4. Отправляем POST-запрос с данными и настройками прокси
|
||||||
response = requests.post(api_url, json=data, proxies=proxies, timeout=400)
|
response = requests.post(api_url, json=data, proxies=proxies, timeout=90)
|
||||||
|
|
||||||
# Проверяем, не вернул ли сервер ошибку (например, 4xx или 5xx)
|
# Проверяем, не вернул ли сервер ошибку (например, 4xx или 5xx)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|||||||
@@ -3,13 +3,6 @@ from g4f.client import Client
|
|||||||
from .base_provider import BaseLLMProvider
|
from .base_provider import BaseLLMProvider
|
||||||
|
|
||||||
class Gpt4FreeProvider(BaseLLMProvider):
|
class Gpt4FreeProvider(BaseLLMProvider):
|
||||||
"""
|
|
||||||
Провайдер для работы с моделью GPT через библиотеку gpt4free.
|
|
||||||
|
|
||||||
Этот класс реализует интерфейс BaseLLMProvider и предоставляет возможность
|
|
||||||
взаимодействия с различными LLM через сервис gpt4free, который не требует
|
|
||||||
API ключа для работы.
|
|
||||||
"""
|
|
||||||
# gpt4free не требует API ключа
|
# gpt4free не требует API ключа
|
||||||
def __init__(self, api_key: str | None = None):
|
def __init__(self, api_key: str | None = None):
|
||||||
super().__init__(api_key)
|
super().__init__(api_key)
|
||||||
@@ -17,18 +10,6 @@ class Gpt4FreeProvider(BaseLLMProvider):
|
|||||||
|
|
||||||
|
|
||||||
def generate(self, model: str, system_prompt: str, user_prompt: str, temp: float):
|
def generate(self, model: str, system_prompt: str, user_prompt: str, temp: float):
|
||||||
"""
|
|
||||||
Генерирует ответ от модели GPT с использованием gpt4free.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model (str): Название модели для генерации ответа
|
|
||||||
system_prompt (str): Системное сообщение для контекста
|
|
||||||
user_prompt (str): Пользовательский запрос
|
|
||||||
temp (float): Температура генерации ( controls randomness of responses)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: Кортеж из двух одинаковых строк - сгенерированного ответа и его копии
|
|
||||||
"""
|
|
||||||
# temp в g4f может работать не для всех внутренних провайдеров
|
# temp в g4f может работать не для всех внутренних провайдеров
|
||||||
try:
|
try:
|
||||||
response = self.client.chat.completions.create(
|
response = self.client.chat.completions.create(
|
||||||
|
|||||||
@@ -1,34 +1,23 @@
|
|||||||
import requests
|
import openai
|
||||||
from .base_provider import BaseLLMProvider
|
from .base_provider import BaseLLMProvider
|
||||||
|
|
||||||
class IoNetProvider(BaseLLMProvider):
|
class IoNetProvider(BaseLLMProvider):
|
||||||
def __init__(self, api_key: str):
|
def __init__(self, api_key: str):
|
||||||
super().__init__(api_key)
|
super().__init__(api_key)
|
||||||
self.base_url = 'https://api.intelligence.io.solutions/api/v1'
|
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):
|
def generate(self, model: str, system_prompt: str, user_prompt: str, temp: float):
|
||||||
url = f"{self.base_url}/chat/completions"
|
response = self.client.chat.completions.create(
|
||||||
headers = {
|
model=model,
|
||||||
"Content-Type": "application/json",
|
messages=[
|
||||||
"Authorization": f"Bearer {self.api_key}"
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"model": model,
|
|
||||||
"messages": [
|
|
||||||
{'role': 'system', 'content': system_prompt},
|
{'role': 'system', 'content': system_prompt},
|
||||||
{'role': 'user', 'content': user_prompt},
|
{'role': 'user', 'content': user_prompt},
|
||||||
],
|
],
|
||||||
"temperature": temp
|
temperature=temp,
|
||||||
}
|
stream=False
|
||||||
|
)
|
||||||
try:
|
text = str(response.choices[0].message.content)
|
||||||
response = requests.post(url, headers=headers, json=data)
|
return text, text # Возвращаем как чистый текст, так и Markdown
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
result = response.json()
|
|
||||||
text = str(result['choices'][0]['message']['content'])
|
|
||||||
return text, text # Возвращаем как чистый текст, так и Markdown
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
raise Exception(f"Ошибка при запросе к IO.net API: {e}")
|
|
||||||
19
test.py
Normal file
19
test.py
Normal 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("---")
|
||||||
Reference in New Issue
Block a user