Связку из Ollama и OpenWebUI я использую больше года, и она является моим рабочим инструментом в части работы с документацией и контентом и по-настоящему ускорила работу по локализации документации HOSTKEY на другие языки. Но жажда исследований не отпускает, и с появлением относительно вменяемой документации к API у OpenWebUI возникло желание написать что-то, автоматизирующее работу. Например, перевод документации из командной строки.
Идея
В HOSTKEY клиентская документация собирается в Material for MkDocs и в исходниках, хранящихся в GIT, представляет собой набор файлов в Markdown формате. А раз мы имеем текстовые файлы, то кто мешает мне не копировать их текст из VS Code и вставлять в чат OpenWebUI в браузере, а запускать скрипт из командной строки, который бы отправлял файл со статьей нейросетевой модели, получал перевод и записывал его назад в файл.
В теории в дальнейшем так можно было бы сделать масс-обработку файлов и запускать автоматизированный черновой перевод на новые языки всех массивов или при создании статьи на одном языке одной командой переводить и клонировать их на множество других. С учетом роста числа переводов (сейчас это русский, английский, турецкий, в работе французский, а в планах испанский и китайский) всё это ускорило бы работу отдела документации. Поэтому разрабатываем план:
Берем исходный файл .md файл;
Скармливаем его нейросетевой модели;
Получаем перевод;
Записываем файл с переводом;
Приложениями для ИИ, машинного обучения и науки о данных на GPU-серверах с картами NVIDIA | |
AI & Машинное обучение | Наука о данных |
Изучаем API
Первый возникающий вопрос: а зачем на OpenWebUI, когда можно сразу «скормить» файл Ollama? Да, это можно сделать, но, забегая вперед, скажу, что использование «прокладки» в виде OpenWebUI было правильным решением. Причем API Ollama еще хуже документировано, чем OpenWebUI.
Описание API OpenWebUI доступно в swagger формате по адресу https://<IP или домен инстанса>/docs/. Из него видно, что вы можете управлять как Ollama, так и самим OpenWebUI и обращаться к нейросетевым моделям в синтаксисе API от OpenAI.

Последнее спасло меня, так как понять, какие параметры и как передавать, было не очень понятно, и пришлось обратиться к документации по API от OpenAI.
Что в итоге? Нам нужно начать чат и передать в модель системный промт, в котором объяснить ей, что делать, а затем текст для перевода, а также параметры, такие как температуру и размер контекста (max_tokens).
В синтаксисе API от OpenAI нам надо сделать POST запрос к OpenWebUI на <адрес OpenWebUI>/ollama/v1/chat/completions, содержащий следующие поля:
Authorization: Bearer <API ключ доступа к OpenWebUI> Content-Type: application/json data body: '{ model: <нужная нам модель>, messages: [ { role: "system", content: <системный промт> }, { role: "user", content: <наш файл для перевода> } ], temperature: 0.6 max_tokens: 16384 }'
Как видно, тело запроса нужно передавать в JSON-формате, и в нем же мы получим ответ.
Писать я решил всё в виде bash-скрипта (для меня универсальное решение, так как можно запускать скрипт и на удаленном Linux-сервере, и локально даже из Windows через WSL), поэтому будем использовать cURL в Ubuntu 22.04. Для работы с JSON-форматом ставлю утилиту jq.
Далее создаю в OpenWebUI пользователя для нашего переводчика, получаю для него API-ключ, устанавливаю несколько нейросетевых моделей для теста и... у меня ничего не получается.
Версия 1.0
Как я писал ранее, нам необходимо сформировать data-часть запроса в JSON-формате. Основной код скрипта, который принимает параметр в виде имени файла для перевода и отправляет запрос, а затем расшифровывает ответ, следующий:
local file=$1 # Read the content of the .md file content=$(<"$file") # Prepare JSON data for the request, including your specified prompt request_json=$(jq -n \ --arg model "gemma2:latest" \ --arg system_content "Operate as native translator from US-EN to TR. I will provide you text in Markdown format for translate. The text is related to IT.\nFollow this instructions:\n\n- Do not change Markdown format.\n- Translate text, considering the specific terminology and features.\n- Do not provide a description of how and why you made such a translation.\ '{ model: $model, messages: [ { role: "system", content: $system_content }, { role: "user", content: $content } ], temperature: 0.6, max_tokens: 16384 }') # Send POST request to the API response=$(curl -s -X POST "$API_URL" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ --data "$request_json") # Extract translated content from the response (assuming it's in 'choices[0].message.content') translated_content=$(echo "$response" | jq -r '.choices[0].message.content')
Как видно, я использовал модель gemma2 на 9B, системный промт для перевода с английского на турецкий язык и просто передал в запросе содержимое файла в Markdown формате. API_URL указывает на http://<IP адреc OpenWebUI:Порт>/ollama/v1/chat/completion.
Тут и была моя первая ошибка — необходимо было подготовить текст для JSON формата. Для этого в скрипте нужно было поправить его начало:
# Read the content of the .md file content=$(<"$file") # Escape special characters in the content for JSON content_cleaned=$(echo "$content" | sed -e 's/\r/\r\\n/g' -e 's/\n/\n\\n/g' -e 's/\t/\\t/g' -e 's/"/\\"/g' -e 's/\\/\\\\/g') # Properly escape the content for JSON escaped_content=$(jq -Rs . <<< "$content_cleaned")
Экранировав специальные символы и переведя .md файл в JSON, а в теле формирования запроса добавить новый аргумент.
--arg user_content "$escaped_content" \
Который и передавать в роли "user".
Дооформляем скрипт и пытаемся улучшить промт.
Промт для перевода
Изначальный промт для переводчика у меня был такой, как в примере. Да, он достаточно неплохо переводил технический текст с турецкого на английский, но были свои проблемы.
Мне необходимо было единообразие перевода определенных конструкций Markdown-разметки, таких как заметки, примечания и т. п. Также хотелось, чтобы переводчик не переводил на турецкий UX-элементы как системы управления серверами Invapi (он у нас пока что на английском и русском), так и интерфейсов ПО, потому что с большим числом языков поддержка локализованных версий превращалась в мини-ад. Сложности добавляло еще то, что в документации используется нестандартные конструкции для кнопок в виде жирного зачеркнутого текста КНОПКА (конструкция ** **). Поэтому в OpenWebUI был отлажен системный промт следующего вида:
You are native translator from English to Turkish.
I will provide you text in Markdown format for translate. The text is related to IT.
Follow these instructions:
- Do not change Markdown format.
- Translate text, considering the specific terminology and features.
- Do not provide a description of how and why you made such a translation.
- Keep on English box, panels, menu and submenu names, buttons names and other UX elements in tags '** **' and '\~\~** **\~\~'.
- Use the following Markdown constructs: '!!! warning "Dikkat"', '!!! info "Bilgi"', '!!! note "Not"', '??? example'. Translate 'Password" as 'Şifre'.
- Translate '## Deployment Features' as '## Çalıştırma Özellikleri'.
- Translate 'Documentation and FAQs' as 'Dokümantasyon ve SSS'.
- Translate 'To install this software using the API, follow [these instructions](../../apidocs/index.md#instant-server-ordering-algorithm-with-eqorder_instance).' as 'Bu yazılımı API kullanarak kurmak için [bu talimatları](https://hostkey.com/documentation/apidocs/#instant-server-ordering-algorithm-with-eqorder_instance) izleyin.'
Теперь этот промт надо было проверить на стабильность на различных моделях, так как нужно было добиться и достаточно качественного перевода, и не пожертвовать скоростью. Gemma2 9B хорошо справляется с переводом, но упорно игнорировала требование не переводить UX-элементы.
Модель DeepSeekR1 в версии 14B также давала большое число ошибок, а местами вообще переключалась на иероглифы. Из всех моделей лучше всего показала себя Phi4-14B. Модели с большим числом параметров было использовать сложнее, так как всё «крутится» на сервере с A5000 с 24 Гб видеопамяти. Единственное, что я взял вместо дефолтной модели Phi4-14B с квантизацией q4 модель в менее сжатом формате (q8).
полная версия скрипта с дебаг-вставками
#!/bin/bash # Check for the presence of a parameter with a file path if [ -z "$1" ]; then echo "Error: Please provide the full path to the file for translation." exit 1 fi # Check if the file exists if [ ! -f "$1" ]; then echo "Error: File $1 does not exist." exit 1 fi # API parameters API_URL="http://<open_webui_ip:port>/api/chat/completions" API_KEY="sk-*****************************" # Check server availability if ! curl -s -o /dev/null -I -w "%{http_code}" "$API_URL" | grep -q "200"; then echo "Error: API server is not reachable or returned an error." exit 1 fi # Translation function using ollama translate_file() { local file=$1 # Read the content of the .md file content=$(<"$file") # Escape special characters in the content for JSON content_cleaned=$(echo "$content" | sed -e 's/\r/\r\\n/g' -e 's/\n/\n\\n/g' -e 's/\t/\\t/g' -e 's/"/\\"/g' -e 's/\\/\\\\/g') # Properly escape the content for JSON escaped_content=$(jq -Rs . <<< "$content_cleaned") # Prepare JSON data for the request, including your specified prompt request_json=$(jq -n \ --arg model "entrtranslator" \ --arg user_content "$escaped_content" \ '{ model: $model, messages: [ { role: "user", content: $user_content } ], temperature: 0.6, max_tokens: 16384, stream: false }') # Debug: Print the JSON request payload # echo "Debug: JSON Request Payload:" # echo "$request_json" # echo "---------------------------" # Send POST request to the API response=$(curl -s -X POST "$API_URL" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ --data "$request_json") # Debug: Print the raw API response # echo "Debug: Raw API Response:" # echo "$response" # echo "---------------------------" # Check if the request was successful if [ $? -ne 0 ]; then echo "Error: Failed to translate $file" return 1 fi # Extract translated content from the response (assuming it's in 'choices[0].message.content') translated_content=$(echo "$response" | jq -r '.choices[0].message.content') # Debug: Print the extracted translated content # echo "Debug: Extracted Translated Content:" # echo "$translated_content" # echo "---------------------------" # Check if the translation was successful if [ -z "$translated_content" ] || [ "$translated_content" == "null" ]; then echo "Error: Translation failed for $file (empty or null content)" return 1 fi translated_content=$(echo "$translated_content" | sed -e 's/^```markdown//') # Save the translated content to a new file in the same directory translated_file="$(dirname "$file")/translated_$(basename "$file")" echo "$translated_content" > "$translated_file" echo "Translation for $file completed." } # Process the provided file file="$1" echo "Processing file $file" cp "$file" "${file}.en" if translate_file "$file"; then mv "$(dirname "$file")/translated_$(basename "$file")" "$file" fromdos "$file" else echo "Failed to translate $file, original file kept." fi
Результат тестов
Всё заработало. Но с некоторыми оговорками. Основная проблема была в том, что новые запросы почему-то не перезапускали сессию чата, и модель жила в прежнем контексте и через пару-тройку файлов теряла системный промт. В результате, если первые пара запусков давали достаточно вменяемый перевод, то потом модель переставала воспринимать инструкции и вообще оставляла текст на английском. Добавление параметра stream: false ситуацию не спасало.
Вторая проблема — это галлюцинации, но в части игнорирования инструкций «не переводить UX». Я пока что не смог добиться стабильности в данном вопросе, и если в чате OpenWebUI я могу «ткнуть носом» модель в то, что она зачем-то перевела название кнопок и меню, и она с 2–3-го раза выдавала нужное, то тут необходимо было только запускать скрипт заново, и срабатывал он иногда раза с пятого-шестого.
Третья проблема — это тюнинг промта. Если в OpenWebUI я мог создать кастомный промт и через раздел Workspace — Promts задать ему слеш-команду вида /en_tr, то в скрипте нужно было переписывать код, причем в не очень удобном формате. То же касается и параметров модели.
Версия 2.0
Поэтому было решено пойти другим путем. В OpenWebUI можно задать кастомные модели-агенты, в которых прописать как системный промт, так и гибко настроить их параметры (и даже использовать RAG) и права. Поэтому я создал агента-переводчика в разделе Workspace — Models (название модели написано мелко и будет «entrtranslator»).

Если попробовать подставить новую модель в текущий скрипт, то он выдаст ошибку. Это происходит потому, что прежний вызов просто передавал параметры в Ollama через OpenWebUI, для которой «модели» entrtranslator просто не существует. Изучение API OpenWebUI методом проб и ошибок привело к другому вызову самого OpenWebUI: /api/chat/completions.
Теперь вызов нашего нейросетевого переводчика можно записать так:
local file=$1 # Read the content of the .md file content=$(<"$file") # Escape special characters in the content for JSON content_cleaned=$(echo "$content" | sed -e 's/\r/\r\\n/g' -e 's/\n/\n\\n/g' -e 's/\t/\\t/g' -e 's/"/\\"/g' -e 's/\\/\\\\/g') # Properly escape the content for JSON escaped_content=$(jq -Rs . <<< "$content_cleaned") # Prepare JSON data for the request, including your specified prompt request_json=$(jq -n \ --arg model "entrtranslator" \ --arg user_content "$escaped_content" \ '{ model: $model, messages: [ { role: "user", content: $user_content } ], temperature: 0.6, max_tokens: 16384, stream: false }') # Send POST request to the API response=$(curl -s -X POST "$API_URL" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ --data "$request_json") # Extract translated content from the response (assuming it's in 'choices[0].message.content') translated_content=$(echo "$response" | jq -r '.choices[0].message.content')
Где API_URL принимает вид http://<IP адреc OpenWebUI:Порт>/api/chat/completions.
Теперь у вас есть возможность гибко настраивать параметры и промт через веб-интерфейс, а также использовать данный скрипт для переводов на другие языки.
Этот способ сработал и позволяет создавать ИИ-агентов для использования в bash-скриптах не только для перевода, но и для других нужд. Процент «непереводов» снизился, и осталась только проблема, когда модель не хочет игнорировать перевод UX-элементов.
Что дальше?
Дальше стоит задача добиться большей стабильности работы, хотя уже даже сейчас можно работать с текстами из интерфейса командной строки, и модель не срабатывает только на больших текстах (больше 16K ставить не позволяет видеопамять, модель начинает тормозить). Это можно сделать как улучшением промта, так и тюнингом параметров модели, которых достаточно много.

Всё это позволит запустить автоматическое создание черновиков переводов на всех поддерживаемых языках, как только будет создан текст на английском языке.
Ну а далее есть идея подключить базу знаний с переводом элементов интерфейсов Invapi и других значений элементов меню на сайте (и ссылок на них), чтобы при переводе не приходилось вручную править ссылки и наименования в статьях. Но работа с RAG в OpenWebUI через API — это тема для отдельной статьи.
P.S. После написания данной статьи была анонсирована модель Gemma3, которая возможно займет место Phi4 в переводчиках, так как она поддерживает 140 языков при контексте до 128K.
