diff --git a/app.py b/app.py
index 6f0eb48..95ba445 100644
--- a/app.py
+++ b/app.py
@@ -1,13 +1,7 @@
import gradio as gr
-
-# Загрузка параметров конфигурации
from config import *
-
-# Подгрузка сервисов
from services.llm_factory import get_llm_provider
from services.fasterWhisper import FasterWhisper
-
-# Загрузка доп. модулей
from handlers.gradioHandler import GradioHandlers
from handlers.fileHandlers import FileHandlers
from handlers.convertMdToPdf import ConvertMdToPdf
@@ -17,16 +11,9 @@ gh = GradioHandlers(get_llm_provider, ConvertMdToPdf, FileHandlers, FasterWhispe
def main():
with gr.Blocks() as demo:
- gr.HTML('''
-
-
- Faster Whisper WebUI
-
-
- ''')
+ gr.HTML('Faster Whisper WebUI
')
with gr.Row():
- # Вкладка с основным взаимодействием
with gr.Tab('Actions'):
isPipelineEnabledCheckbox = gr.Checkbox(label='is pipeline enabled', value=True, interactive=True)
@@ -36,79 +23,64 @@ def main():
audioFiles = gr.Files(label='Load audio for transcribe', type="filepath")
images = gr.Files(label='Upload images', file_types=['image'])
recognizeBtn = gr.Button('recognize and integrate', variant='primary')
-
with gr.Accordion(label='Recognized text'):
recognizedText = gr.TextArea(label='')
with gr.Accordion(label='LLM'):
with gr.Column():
refineTextBtn = gr.Button('refine text', variant='secondary', interactive=False)
-
with gr.Accordion(label='Refined text raw'):
refinedText = gr.Textbox(label='', show_copy_button=True)
-
with gr.Accordion(label='Refined text md formated'):
refinedTextMD = gr.Markdown(label='')
- # Вкладка с настройками
with gr.Tab('Settings'):
with gr.Column():
- # Первое поле на всю ширину в акордионе настроек
with gr.Accordion('File settings'):
saveFileCheckbox = gr.Checkbox(label='save file', value=True, interactive=True)
- filename = gr.Textbox(label='Output filename', value='output.txt', interactive=True)
+ filename = gr.Textbox(label='Output filename', value='output.md', 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.Row():
- # Левая колонка в акордионе
with gr.Column():
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)
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)
noSpeechThreshold = gr.Number(label='no_speech_threshold', value=0.5, 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)
speechPadMs = gr.Number(label='speech_pad_ms', value=200, interactive=True)
-
with gr.Accordion(label='Temperature'):
temp0 = gr.Number(label='temp_0', value=0.0, interactive=True)
temp1 = gr.Number(label='temp_1', value=0.2, interactive=True)
temp2 = gr.Number(label='temp_2', value=0.4, interactive=True)
- # Нижний акордион настроек для api ключа llm
with gr.Accordion(label='LLM settings'):
apiKey = gr.Textbox(label='API key (required for io.net, Gemini)', value=DEFAULT_API_KEY, interactive=True)
-
with gr.Accordion(label='System prompt'):
systemPrompt = gr.Textbox(label='', value=DEFAULT_SYSTEM_PROMPT, interactive=True)
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(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)
+
+ # Настройки Custom провайдера
+ with gr.Accordion(label='Custom Provider Settings', open=True):
+ customBaseUrl = gr.Textbox(
+ label='Base URL',
+ value='http://127.0.0.1:1234/v1/',
+ interactive=True,
+ visible=False # Скрыто по умолчанию
+ )
+ # Обработчики событий
isPipelineEnabledCheckbox.change(gh.updateButton, inputs=[isPipelineEnabledCheckbox], outputs=refineTextBtn)
saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filename)
saveFileCheckbox.change(gh.updateTextbox, inputs=saveFileCheckbox, outputs=filenamePdf)
@@ -119,33 +91,38 @@ def main():
vadFilter, minSilenceDurationMs, speechPadMs, temp0, temp1, temp2,
wordTimestamps, noSpeechThreshold, conditionOnPreviousText, gr.State(GLUED_AUDIO_FILENAME), gr.State(OUTPUT_PATH)],
outputs=[recognizedText],
- )
+ )
- # Если пайплайн включен то тогда делаем автоматически
- # автоматический пайплайн
+ # --- ИСПРАВЛЕНИЕ: ДОБАВЛЕН customBaseUrl В INPUTS ---
recognizedText.change(
gh.generateByCondition,
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature,
- isPipelineEnabledCheckbox, gr.State("change"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)],
+ isPipelineEnabledCheckbox, gr.State("change"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH), customBaseUrl],
outputs=[refinedText, refinedTextMD]
)
-
-
- # ручной запуск по кнопке
+ # Обновление выпадающего списка моделей и поля API ключа
llmProvider.change(
gh.update_model_dropdown,
inputs=llmProvider,
outputs=[llmModel, apiKey]
)
+
+ # Переключение видимости URL для Custom провайдера
+ llmProvider.change(
+ fn=gh.toggle_custom_url,
+ inputs=llmProvider,
+ outputs=[customBaseUrl]
+ )
+
refineTextBtn.click(
gh.generateByCondition,
inputs=[apiKey, llmProvider, llmModel, systemPrompt, recognizedText, llmTemperature,
- isPipelineEnabledCheckbox, gr.State("click"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH)],
+ isPipelineEnabledCheckbox, gr.State("click"), saveFileCheckbox, filename, filenamePdf, gr.State(OUTPUT_PATH), customBaseUrl],
outputs=[refinedText, refinedTextMD]
)
demo.launch()
if __name__ == '__main__':
- main()
+ main()
\ No newline at end of file
diff --git a/config.py b/config.py
index b710fc6..708ac2f 100644
--- a/config.py
+++ b/config.py
@@ -14,7 +14,7 @@ DEFAULT_API_KEY=IO_API_KEY
# Словарь провайдеров и их моделей
-LLM_PROVIDERS = ['io.net', 'Gemini', 'gpt4free']
+LLM_PROVIDERS = ['io.net', 'Gemini', 'gpt4free', 'Custom']
LLM_MODELS = {
'io.net': [
'openai/gpt-oss-120b', 'Qwen/Qwen3-235B-A22B-Thinking-2507',
@@ -30,7 +30,7 @@ LLM_MODELS = {
'gemini-2.5-flash',
'gemini-2.5-flash-lite'
],
- 'gpt4free': [ # Модели могут меняться, проверьте документацию g4f
+ 'gpt4free': [
'default',
'gpt-4',
'sonar-reasoning',
@@ -41,7 +41,14 @@ LLM_MODELS = {
'gpt-4o-mini',
'deepseek-r1',
'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',
+ ],
}
# Задаем выходную директорию
diff --git a/handlers/gradioHandler.py b/handlers/gradioHandler.py
index fa57382..b64c439 100644
--- a/handlers/gradioHandler.py
+++ b/handlers/gradioHandler.py
@@ -1,50 +1,52 @@
-from config import LLM_MODELS # Импортируем словарь моделей
+from config import LLM_MODELS, GEMINI_API_KEY, IO_API_KEY
import gradio as gr
-from config import GEMINI_API_KEY, IO_API_KEY
class GradioHandlers:
def __init__(self, llm_factory, ConvertMdToPdf, FileHandlers, FasterWhisper, GlueAudio):
- # Объект для работы с файлами
self.fh = FileHandlers()
self.ga = GlueAudio()
self.ConvertMdToPdf = ConvertMdToPdf()
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,
- wordTimestamps, noSpeechThreshold, conditionOnPreviousText, filename, outPath
- ):
+ wordTimestamps, noSpeechThreshold, conditionOnPreviousText, filename, outPath):
try:
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_filename=filename
)
except (FileNotFoundError, RuntimeError) as e:
- # Если FFmpeg не найден или произошла ошибка, сообщаем пользователю
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)
- # Функция улучшения текста
+ # Добавил аргумент custom_base_url в конец
def generateByCondition(self, api_key, llm_provider,
llm_model, system_prompt, recognized_text,
llm_temperature, is_pipeline_enabled, trigger,
- isSaveFile, filename, filenamePdf, output_path):
+ isSaveFile, filename, filenamePdf, output_path, custom_base_url):
+
try:
- # Получаем нужный провайдер через фабрику
- provider = self.llm_factory(llm_provider, api_key)
+ if llm_provider == "Custom":
+ # Передаем base_url только для Custom
+ 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:
- # Если API ключ не предоставлен для нужного провайдера, выводим ошибку
gr.Warning(str(e))
return gr.skip(), gr.skip()
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)
if isSaveFile:
self.fh.saveFile(filenamePdf, pdf, output_path)
@@ -56,30 +58,30 @@ class GradioHandlers:
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' напрямую.
- if provider == 'io.net': return gr.update(choices=models, value=default_value), gr.update(label='API key (required for io.net, Gemini)', value=IO_API_KEY, interactive=True)
- if provider == 'Gemini': return gr.update(choices=models, value=default_value), gr.update(label='API key (required for Oio.net, Gemini)', value=GEMINI_API_KEY, interactive=True)
- if provider == 'gpt4free': return gr.update(choices=models, value=default_value), gr.update(label='API key (required for Oio.net, Gemini)', value="", interactive=True)
+ # Обновляем список моделей и настройки поля API Key
+ if provider == 'io.net':
+ 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', 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):
- if not isChecked:
- variant = 'primary'
- else:
- variant = 'secondary'
+ variant = 'secondary' if isChecked else 'primary'
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):
return gr.update(visible=isChecked)
\ No newline at end of file
diff --git a/handlers/metadataHandler.py b/handlers/metadataHandler.py
new file mode 100644
index 0000000..9c3c89e
--- /dev/null
+++ b/handlers/metadataHandler.py
@@ -0,0 +1,21 @@
+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
\ No newline at end of file
diff --git a/pandoc.txt b/pandoc.txt
new file mode 100644
index 0000000..9013885
--- /dev/null
+++ b/pandoc.txt
@@ -0,0 +1,7 @@
+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\
diff --git a/services/llm_factory.py b/services/llm_factory.py
index 32aed24..cecbdbe 100644
--- a/services/llm_factory.py
+++ b/services/llm_factory.py
@@ -3,8 +3,9 @@ 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
+from services.llm_providers.custom_provider import CustomProvider
-def get_llm_provider(provider_name: str, api_key: str | None) -> BaseLLMProvider:
+def get_llm_provider(provider_name: str, api_key: str | None = None, base_url: str | None = None) -> BaseLLMProvider:
"""
Фабричная функция для получения экземпляра провайдера LLM.
"""
@@ -18,5 +19,9 @@ def get_llm_provider(provider_name: str, api_key: str | None) -> BaseLLMProvider
return GeminiProvider(api_key)
elif provider_name == 'gpt4free':
return Gpt4FreeProvider()
+ elif provider_name == 'Custom':
+ if not base_url:
+ raise ValueError("Base URL обязателен для Custom провайдера")
+ return CustomProvider(api_key, base_url) # base_url будет установлен позже
else:
raise ValueError(f"Неизвестный провайдер: {provider_name}")
\ No newline at end of file
diff --git a/services/llm_providers/custom_provider.py b/services/llm_providers/custom_provider.py
new file mode 100644
index 0000000..5e7cc50
--- /dev/null
+++ b/services/llm_providers/custom_provider.py
@@ -0,0 +1,44 @@
+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}"
\ No newline at end of file
diff --git a/services/llm_providers/gemini_provider.py b/services/llm_providers/gemini_provider.py
index f87ac36..dca4f39 100644
--- a/services/llm_providers/gemini_provider.py
+++ b/services/llm_providers/gemini_provider.py
@@ -41,7 +41,7 @@ class GeminiProvider(BaseLLMProvider):
try:
# 4. Отправляем POST-запрос с данными и настройками прокси
- response = requests.post(api_url, json=data, proxies=proxies, timeout=90)
+ response = requests.post(api_url, json=data, proxies=proxies, timeout=400)
# Проверяем, не вернул ли сервер ошибку (например, 4xx или 5xx)
response.raise_for_status()
diff --git a/services/llm_providers/gpt4free_provider.py b/services/llm_providers/gpt4free_provider.py
index 25c954c..6b4bb27 100644
--- a/services/llm_providers/gpt4free_provider.py
+++ b/services/llm_providers/gpt4free_provider.py
@@ -3,6 +3,13 @@ from g4f.client import Client
from .base_provider import BaseLLMProvider
class Gpt4FreeProvider(BaseLLMProvider):
+ """
+ Провайдер для работы с моделью GPT через библиотеку gpt4free.
+
+ Этот класс реализует интерфейс BaseLLMProvider и предоставляет возможность
+ взаимодействия с различными LLM через сервис gpt4free, который не требует
+ API ключа для работы.
+ """
# gpt4free не требует API ключа
def __init__(self, api_key: str | None = None):
super().__init__(api_key)
@@ -10,6 +17,18 @@ class Gpt4FreeProvider(BaseLLMProvider):
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 может работать не для всех внутренних провайдеров
try:
response = self.client.chat.completions.create(
diff --git a/services/llm_providers/ionet_provider.py b/services/llm_providers/ionet_provider.py
index 9840f71..c5d3a8d 100644
--- a/services/llm_providers/ionet_provider.py
+++ b/services/llm_providers/ionet_provider.py
@@ -1,23 +1,34 @@
-import openai
+import requests
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/'
- )
+ self.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=[
+ url = f"{self.base_url}/chat/completions"
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {self.api_key}"
+ }
+
+ data = {
+ "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
\ No newline at end of file
+ "temperature": temp
+ }
+
+ try:
+ response = requests.post(url, headers=headers, json=data)
+ 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}")
\ No newline at end of file