Как стать автором
Обновить
66.55
HOSTKEY
IT-инфраструктура: сервера, VPS, GPU, коло

Нейросетевой переводчик в командной строке, или Приручаем API Ollama и OpenWebUI

Уровень сложностиСредний
Время на прочтение12 мин
Количество просмотров533

Связку из Ollama и OpenWebUI я использую больше года, и она является моим рабочим инструментом в части работы с документацией и контентом и по-настоящему ускорила работу по локализации документации HOSTKEY на другие языки. Но жажда исследований не отпускает, и с появлением относительно вменяемой документации к API у OpenWebUI возникло желание написать что-то, автоматизирующее работу. Например, перевод документации из командной строки.

Идея

В HOSTKEY клиентская документация собирается в Material for MkDocs и в исходниках, хранящихся в GIT, представляет собой набор файлов в Markdown формате. А раз мы имеем текстовые файлы, то кто мешает мне не копировать их текст из VS Code и вставлять в чат OpenWebUI в браузере, а запускать скрипт из командной строки, который бы отправлял файл со статьей нейросетевой модели, получал перевод и записывал его назад в файл.

В теории в дальнейшем так можно было бы сделать масс-обработку файлов и запускать автоматизированный черновой перевод на новые языки всех массивов или при создании статьи на одном языке одной командой переводить и клонировать их на множество других. С учетом роста числа переводов (сейчас это русский, английский, турецкий, в работе французский, а в планах испанский и китайский) всё это ускорило бы работу отдела документации. Поэтому разрабатываем план:

  1. Берем исходный файл .md файл;

  2. Скармливаем его нейросетевой модели;

  3. Получаем перевод;

  4. Записываем файл с переводом;

Приложениями для ИИ, машинного обучения и науки о данных на GPU-серверах с картами NVIDIA

AI & Машинное обучение
🔹PyTorch
🔹AI-чатбот
🔹TensorFlow
🔹Apache Spark
🔹ComfyUI

Наука о данных
🔹Jupyter Notebook
🔹JupyterLab
🔹Anaconda
🔹Apache Airflow

Заказать сервер

Изучаем 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.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+4
Комментарии0

Публикации

Информация

Сайт
www.hostkey.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия

Истории