搜索

blogia

在 Python 脚本中集成 OpenAI o1-preview 模型并处理 LaTeX 表达式

在 Python 脚本中集成 OpenAI o1-preview 模型并处理 LaTeX 表达式

了解如何在 Python 脚本中集成新的 OpenAI o1-preview 模型,以增强您的人工智能项目。该脚本允许您使用 o1-preview 模型与 OpenAI API 交互,并通过内置的网页抓取功能在提示中包含网页内容。此外,它还能正确处理模型回答中的 LaTeX 数学表达式,将其转换为终端可读的 Unicode 文本。

介绍

2024 年 9 月 12 日,OpenAI 发布了其新一代 AI 模型系列,名为 OpenAI o1。这些模型在给出回答之前会进行更深入的推理,使它们能够解决科学、编程和数学中的复杂问题。o1-preview 模型在这些领域表现尤为出色,优于之前的模型如 gpt-4o

脚本的要点:

  • 集成 o1-preview 模型:脚本默认使用 o1-preview 模型,从而提供先进的推理能力。
  • 内置网页抓取:可以提取网页内容以丰富提示的上下文。
  • LaTeX 表达式处理:将回答中的数学表达式转换为终端可读的 Unicode 文本。
  • 可定制:脚本允许选择 OpenAI 模型,并可根据不同用例进行调整。

在本文中,我将详解脚本代码,解释其工作原理,并演示一系列复杂的提示示例。

先决条件

在开始之前,请确保您具备以下条件:

  • 已在您的机器上安装 Python 3.x
  • 拥有一把 OpenAI API 密钥。您可以通过在 OpenAI 网站注册获取。
  • 一个 Python 虚拟环境(推荐用于隔离依赖)。
  • 所需的 Python 模块。

配置虚拟环境

为隔离本项目的依赖,建议使用虚拟环境。下面是创建虚拟环境并安装所需依赖的方法:

python3 -m venv env
source env/bin/activate  # Sur Windows, utilisez env\Scripts\activate
pip install openai selenium webdriver-manager pylatexenc

设置 OpenAI API 密钥

将您的 OpenAI API 密钥设置为环境变量:

export OPENAI_API_KEY='votre_clé_api_ici'

'votre_clé_api_ici' 替换为您实际的 API 密钥。

完整脚本代码

以下是完整的 Python 脚本代码:

#!/usr/bin/env python3
import os
import sys
import argparse
import re
from openai import OpenAI
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
from pylatexenc.latex2text import LatexNodes2Text


def get_web_content(url):
    if not url:
        return ""

    try:
        # Configure les options de Chrome
        chrome_options = Options()
        # Ne pas utiliser le mode headless pour éviter les problèmes de vérification humaine

        # Utilise ChromeDriverManager pour gérer l\'installation de ChromeDriver
        driver = webdriver.Chrome(
            service=Service(ChromeDriverManager().install()), options=chrome_options
        )

        # Charge la page web
        driver.get(url)

        # Récupère le contenu textuel de la page
        web_content = driver.execute_script("return document.documentElement.innerText")

        # Ferme le navigateur
        driver.quit()

        return web_content if web_content else None
    except Exception as e:
        return None


def convert_latex_to_text(latex_str):
    # Convertit les expressions LaTeX en texte Unicode
    return LatexNodes2Text().latex_to_text(latex_str)


def clean_output(content):
    # Trouve toutes les expressions LaTeX dans le contenu et les convertit
    patterns = [r"\\\\[.*?\\\\]", r"\\\(.*?\\\)", r"\$\$.*?\$\$", r"\$.*?\$"]

    for pattern in patterns:
        matches = re.findall(pattern, content, flags=re.DOTALL)
        for match in matches:
            plain_text = convert_latex_to_text(match)
            content = content.replace(match, plain_text)
    return content


def get_response(prompt, client, model="o1-preview"):
    urls = re.findall(r"(https?://\S+)", prompt)
    for url in urls:
        web_content = get_web_content(url)
        if web_content:
            prompt = prompt.replace(url, web_content)
        else:
            return f"Erreur: Le contenu web pour {url} ne peut être récupéré."

    try:
        response = client.chat.completions.create(
            model=model,
            messages=[
                {
                    "role": "user",
                    "content": prompt,
                },
            ],
        )
        first_choice_message = response.choices[0].message
        content = first_choice_message.content
        # Convertit les expressions LaTeX en texte lisible
        cleaned_content = clean_output(content)
        return cleaned_content
    except Exception as e:
        return f"Une erreur est survenue : {e}"


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("prompt", nargs="?", help="Le prompt contenant des URLs")
    parser.add_argument(
        "--model",
        default="o1-preview",
        choices=["gpt-4o", "o1-preview", "o1-mini"],
        help="Le modèle OpenAI à utiliser (par défaut o1-preview)",
    )
    args = parser.parse_args()

    openai_api_key = os.getenv("OPENAI_API_KEY")
    if not openai_api_key:
        raise ValueError(
            "La clé API OPENAI_API_KEY n'est pas définie dans les variables d\'environnement."
        )

    with OpenAI(api_key=openai_api_key) as client:
        prompt = args.prompt or sys.stdin.read()
        response = get_response(prompt, client, model=args.model)
        print(response)


if __name__ == "__main__":
    main()

代码说明

导入所需模块

脚本首先导入以下必要模块:

  • os, sys, argparse, re:标准模块,用于处理环境变量、命令行参数和正则表达式。
  • openai:用于与 OpenAI API 交互的模块。
  • selenium 和 webdriver_manager:用于执行网页抓取。
  • pylatexenc:用于将 LaTeX 表达式转换为可读的 Unicode 文本。

函数 get_web_content

此函数获取网页的文本内容。

def get_web_content(url):
    if not url:
        return ""

    try:
        # Configure les options de Chrome
        chrome_options = Options()
        # Ne pas utiliser le mode headless pour éviter les problèmes de vérification humaine

        # Utilise ChromeDriverManager pour gérer l\'installation de ChromeDriver
        driver = webdriver.Chrome(
            service=Service(ChromeDriverManager().install()), options=chrome_options
        )

        # Charge la page web
        driver.get(url)

        # Récupère le contenu textuel de la page
        web_content = driver.execute_script("return document.documentElement.innerText")

        # Ferme le navigateur
        driver.quit()

        return web_content if web_content else None
    except Exception as e:
        return None

要点:

  • Chrome 选项:脚本不使用无头(headless)模式,以避免某些页面对无头浏览器实施的人机验证问题。
  • ChromeDriverManager:自动管理 ChromeDriver 的安装和更新。
  • 内容提取:使用 JavaScript 提取页面的完整文本。
  • 异常处理:在发生错误时,函数返回 None

函数 convert_latex_to_text

此函数将 LaTeX 表达式转换为 Unicode 文本。

def convert_latex_to_text(latex_str):
    # Convertit les expressions LaTeX en texte Unicode
    return LatexNodes2Text().latex_to_text(latex_str)

要点:

  • 使用库 pylatexenc 将 LaTeX 表达式转换,使数学公式在终端中可读。

函数 clean_output

此函数处理模型的响应,将 LaTeX 表达式进行转换。

def clean_output(content):
    # Trouve toutes les expressions LaTeX dans le contenu et les convertit
    patterns = [r"\\\\[.*?\\\\]", r"\\\(.*?\\\)", r"\$\$.*?\$\$", r"\$.*?\$"]

    for pattern in patterns:
        matches = re.findall(pattern, content, flags=re.DOTALL)
        for match in matches:
            plain_text = convert_latex_to_text(match)
            content = content.replace(match, plain_text)
    return content

要点:

  • 查找 LaTeX 表达式:使用正则表达式识别公式。
  • 转换:将每个公式转换为 Unicode 文本。
  • 替换:用可读的等价文本替换 LaTeX 公式。

函数 get_response

准备提示(prompt),在必要时处理网页抓取,调用 OpenAI API 并清理响应。

def get_response(prompt, client, model="o1-preview"):
    urls = re.findall(r"(https?://\S+)", prompt)
    for url in urls:
        web_content = get_web_content(url)
        if web_content:
            prompt = prompt.replace(url, web_content)
        else:
            return f"Erreur: Le contenu web pour {url} ne peut être récupéré."

    try:
        response = client.chat.completions.create(
            model=model,
            messages=[
                {
                    "role": "user",
                    "content": prompt,
                },
            ],
        )
        first_choice_message = response.choices[0].message
        content = first_choice_message.content
        # Convertit les expressions LaTeX en texte lisible
        cleaned_content = clean_output(content)
        return cleaned_content
    except Exception as e:
        return f"Une erreur est survenue : {e}"

要点:

  • 处理 URL:如果提示中包含 URL,会提取该页面内容并插入提示中。
  • 调用 OpenAI API:将修改后的提示发送到指定的模型。
  • 响应清理:将 LaTeX 表达式转换为便于阅读的形式。

函数 main

处理命令行参数并执行脚本。

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("prompt", nargs="?", help="Le prompt contenant des URLs")
    parser.add_argument(
        "--model",
        default="o1-preview",
        choices=["gpt-4o", "o1-preview", "o1-mini"],
        help="Le modèle OpenAI à utiliser (par défaut o1-preview)",
    )
    args = parser.parse_args()

    openai_api_key = os.getenv("OPENAI_API_KEY")
    if not openai_api_key:
        raise ValueError(
            "La clé API OPENAI_API_KEY n'est pas définie dans les variables d\'environnement."
        )

    with OpenAI(api_key=openai_api_key) as client:
        prompt = args.prompt or sys.stdin.read()
        response = get_response(prompt, client, model=args.model)
        print(response)

要点:

  • 参数:脚本接受一个提示(prompt)和一个模型参数。
  • API 密钥:检查是否已设置 API 密钥。
  • 执行:调用函数 get_response 并打印响应。

脚本执行

if __name__ == "__main__":
    main()

使用示例

提出需要推理的问题

./openai_poc.py "Dans un triangle rectangle, si les côtés adjacents à l'angle droit mesurent 3 cm et 4 cm, calcule la longueur de l'hypoténuse."

要计算一个直角三角形中两条直角边分别为 3 cm 和 4 cm 时斜边的长度,我们使用毕达哥拉斯定理:

Hypoteˊnuse2=(Coˆteˊ 1)2+(Coˆteˊ 2)2\text{Hypoténuse}^2 = (\text{Côté 1})^2 + (\text{Côté 2})^2

将给定数值代入:

Hypoteˊnuse2=32+42=9+16=25\text{Hypoténuse}^2 = 3^2 + 4^2 = 9 + 16 = 25

对两边取平方根:

Hypoteˊnuse=25=5 cm\text{Hypoténuse} = \sqrt{25} = 5 \text{ cm}

因此,斜边的长度为 5 cm。

斜边的长度为 5 cm。

生成网页摘要

./openai_poc.py "Fais-moi un résumé de cette page : https://openai.com/index/introducing-openai-o1-preview/"

OpenAI 于 2024 年 9 月 12 日宣布推出一系列新的 AI 模型,称为 OpenAI o1,该系列模型被设计为在回答前花更多时间进行思考。这些模型能够在复杂任务中进行推理,并解决比以前模型在科学、编程和数学方面更困难的问题。

该系列的首个模型 o1-preview 已可在 ChatGPT 以及 OpenAI API 中使用。同时还提供一个更轻量、更经济的版本 OpenAI o1-mini,它以比 o1-preview 低约 80% 的成本提供有效的编码能力。

计算简单乘法

./openai_poc.py "Quel est le résultat de 15 x 12 ?"

15×1215 \times 12 的结果是 180

解决简单方程

./openai_poc.py "Résous l'équation 2x + 5 = 15."

为了解方程 2x+5=152x + 5 = 15,请按以下步骤操作:

  1. 两边同时减去 5 以隔离含有未知数的项:
2x+55=1552x=10\begin{aligned} 2x + 5 - 5 &= 15 - 5 \\ 2x &= 10 \end{aligned}
  1. 两边同时除以 2 以求解 xx
2x2=102x=5\begin{aligned} \frac{2x}{2} &= \frac{10}{2} \\ x &= 5 \end{aligned}

解: x=5x = 5

计算百分比

./openai_poc.py "Quel est 20% de 250 ?"

25025020%20\% 等于 50

计算过程:

20%×250=(20100)×250=0,2×250=5020\% \times 250 = \left( \frac{20}{100} \right) \times 250 = 0,2 \times 250 = 50

计算圆的面积

./openai_poc.py "Calcule l'aire d'un cercle de rayon 5 cm."

要计算半径为 5 cm 的圆的面积,使用圆面积公式:

A=πr2A = \pi r^2

其中:

  • AA 是圆的面积,
  • rr 是圆的半径。

rr 代入 5 cm:

A=π(5 cm)2=π(25 cm2)=25π cm2\begin{aligned} A &= \pi (5 \text{ cm})^2 \\ &= \pi (25 \text{ cm}^2) \\ &= 25\pi \text{ cm}^2 \end{aligned}

因此,圆的面积是 25π cm225\pi \text{ cm}^2

如果需要近似值,取 π3,1416\pi \approx 3,1416

A25×3,1416 cm22˘24878,54 cm2\begin{aligned} A &≈ 25 \times 3,1416 \text{ cm}^2 \u2248 78,54 \text{ cm}^2 \end{aligned}

所以,圆的面积约为 78,54 cm²

计算简单积分

./openai_poc.py "Calcule l'intégrale de 0 à 1 de x^2 dx."

要计算函数 x2x^2 在 0 到 1 上的积分,我们对 x2x^2 关于 xx 积分并在上下限 0 和 1 处求值。

计算步骤:

  1. x2x^2 积分:
x2dx=x33+C\int x^2 \, dx = \frac{x^3}{3} + C

其中 C 为积分常数。

  1. 在上下限 0 到 1 之间求值:
01x2dx=[x33]01=(133)(033)\int_0^1 x^2 \, dx = \left[ \frac{x^3}{3} \right]_0^1 = \left( \frac{1^3}{3} \right) - \left( \frac{0^3}{3} \right)
  1. 计算上下限值:
133=13并且033=0\frac{1^3}{3} = \frac{1}{3} \quad \text{并且} \quad \frac{0^3}{3} = 0
  1. 相减得到最终结果:
01x2dx=130=13\int_0^1 x^2 \, dx = \frac{1}{3} - 0 = \frac{1}{3}

结论:

函数 x2x^2 在 0 到 1 上的积分值为 1/31/3

计算平均速度

./openai_poc.py "Si une voiture parcourt 150 km en 3 heures, quelle est sa vitesse moyenne ?"

要计算一辆车在 3 小时内行驶 150 km 的平均速度,使用平均速度公式:

Vitesse moyenne=Distance totaleTemps total\text{Vitesse moyenne} = \frac{\text{Distance totale}}{\text{Temps total}}

代入数值:

Vitesse moyenne=150 km3 heures=50 km/h\text{Vitesse moyenne} = \frac{150 \text{ km}}{3 \text{ heures}} = 50 \text{ km/h}

该车的平均速度为 50 km/h。

经典力学

./openai_poc.py "Un bloc de 5 kg est tiré sur une surface sans frottement par une force de 20 N. Calcule son accélération."

要计算物块的加速度,我们使用牛顿第二定律,表明作用在物体上的合力等于物体质量乘以加速度(F=m×aF = m \times a)。

已知:

  • 物块质量 m=5 kgm = 5 \text{ kg}
  • 作用力 F=20 NF = 20 \text{ N}
  • 无摩擦面(因此没有摩擦力)

将公式重排以求加速度 aa

a=Fma = \frac{F}{m}

代入数值:

a=20 N5 kg=4 m/s2a = \frac{20 \text{ N}}{5 \text{ kg}} = 4 \text{ m/s}^2

答案:物块的加速度为 4 m/s24 \text{ m/s}^2

常微分方程求解

./openai_poc.py "Résous l'équation différentielle dy/dx = x^2 + y^2 avec la condition initiale y(0) = 1."

考虑求解微分方程:

dydx=x2+y2\frac{dy}{dx} = x^2 + y^2

满足初始条件 y(0)=1y(0) = 1。注意该方程为非线性方程,不能用诸如变量分离或常规积分因子等基础方法求解。不过我们可以尝试将其转化为更易处理的形式。

第 1 步:识别方程的类型

该方程为 Riccati 方程,通式为:

dydx=q0(x)+q1(x)y+q2(x)y2\frac{dy}{dx} = q_0(x) + q_1(x) y + q_2(x) y^2

在本例中:

dydx=x2+0y+1y2\frac{dy}{dx} = x^2 + 0 \cdot y + 1 \cdot y^2

第 2 步:转化为二阶线性方程

为解 Riccati 方程,可以使用以下替换:

y=uuy = -\frac{u'}{u}

其中 u=u(x)u = u(x)uu'uuxx 的导数。

计算 yy'

y=uuy = -\frac{u'}{u} y=ddx(uu)y' = -\frac{d}{dx}\left( \frac{u'}{u} \right)

应用商法则得:

y=(uu(u)2u2)y' = -\left( \frac{u'' \cdot u - (u')^2}{u^2} \right)

第 3 步:代入原方程

yyyy' 代入原方程:

y=x2+y2y' = x^2 + y^2 (uu(u)2u2)=x2+(uu)2-\left( \frac{u'' u - (u')^2}{u^2} \right) = x^2 + \left( -\frac{u'}{u} \right)^2

化简:

(uu(u)2u2)=x2+(u)2u2-\left( \frac{u'' u - (u')^2}{u^2} \right) = x^2 + \frac{(u')^2}{u^2}

两边同乘以 u2u^2 消去分母:

(uu(u)2)=x2u2+(u)2-( u'' u - (u')^2 ) = x^2 u^2 + (u')^2

化简得到:

uu+(u)2=x2u2+(u)2-u'' u + (u')^2 = x^2 u^2 + (u')^2 uu=x2u2-u'' u = x^2 u^2 uu=x2u2u'' u = -x^2 u^2

在假设 u0u \neq 0 下除以 uu

u=x2uu'' = -x^2 u

第 4 步:求解二阶线性方程

得到的方程为二阶齐次线性常微分方程:

u+x2u=0u'' + x^2 u = 0

该方程属于系数为变量的线性方程,通常与 Airy 方程相关。其解不能用初等函数表示,而是用特殊函数表示。

方程的通解为:

u(x)=C1Ai(ix)+C2Bi(ix)u(x) = C_1 \cdot \text{Ai}(i x) + C_2 \cdot \text{Bi}(i x)

其中 Ai\text{Ai}Bi\text{Bi} 为第一类和第二类 Airy 函数,ii 为虚数单位(i2=1i^2 = -1)。

第 5 步:回到 y(x)y(x)

记住:

y=uuy = -\frac{u'}{u}

知道 u(x)u(x) 后,通过求导并取负比可以得到 y(x)y(x)

第 6 步:应用初始条件

要确定常数 C1C_1C2C_2,需应用初始条件 y(0)=1y(0) = 1。由于 Airy 函数在复参数下的复杂性,显式计算较为繁琐。

结论

带有初始条件 y(0)=1y(0)=1 的方程解可以用 Airy 特殊函数表示,不能用初等函数显式表示。

最终回答:

微分方程 dydx=x2+y2\frac{dy}{dx} = x^2 + y^2 且满足 y(0)=1y(0) = 1 的解以 Airy 特殊函数给出,无法用初等函数显式表示。

电磁学

./openai_poc.py "Calcule le champ électrique à une distance r d'une charge ponctuelle Q."

点电荷 QQ 在距离 rr 处产生的电场 EE 由库仑定律给出。计算电场的公式为:

E=14πϵ0×Qr2E = \frac{1}{4\pi\epsilon_0} \times \frac{Q}{r^2}

其中:

  • EE 为电场(单位伏特每米,V/m),
  • QQ 为点电荷(单位库仑,C),
  • rr 为到电荷的距离(单位米,m),
  • ϵ0\epsilon_0 为真空介电常数,近似值为 8,854×1012 C2/Nm28,854 \times 10^{-12} \text{ C}^2/\text{N}\cdot\text{m}^2

计算示例:

假设电荷 Q=2×106 CQ = 2 \times 10^{-6} \text{ C}(2 微库仑),欲计算距离 r=0,5 mr = 0,5 \text{ m} 处的电场。

  1. 计算分数前的常数:
14πϵ0=14π×8,854×10128,988×109 Nm2/C2\frac{1}{4\pi\epsilon_0} = \frac{1}{4\pi \times 8,854 \times 10^{-12}} \approx 8,988 \times 10^9 \text{ N}\cdot\text{m}^2/\text{C}^2
  1. 代入公式:
E=(8,988×109)×2×106(0,5)2E = (8,988 \times 10^9) \times \frac{2 \times 10^{-6}}{(0,5)^2} E=(8,988×109)×2×1060,25E = (8,988 \times 10^9) \times \frac{2 \times 10^{-6}}{0,25} E=(8,988×109)×8×106E = (8,988 \times 10^9) \times 8 \times 10^{-6} E=71904 V/mE = 71\,904 \text{ V/m}

结论:

距离为 r=0,5 mr = 0,5 \text{ m} 的点电荷 Q=2×106 CQ = 2 \times 10^{-6} \text{ C} 在该点产生的电场为 E=71904 V/mE = 71\,904 \text{ V/m}

因此,若要计算某一距离处的电场,只需将具体的 QQrr 值代入上述公式。

化学平衡

./openai_poc.py "Explique comment le principe de Le Chatelier s'applique à l'équilibre suivant : N2 + 3H2 ⇌ 2NH3."

勒夏特列原理(Le Chatelier 原理)指出,当处于平衡的系统受到外部扰动(应力)时,系统会作出反应以减小该扰动并建立新的平衡。应用于以下化学平衡:

N2(g)+3H2(g)2NH3(g)N_2(g) + 3H_2(g) \rightleftharpoons 2NH_3(g)

我们可以分析不同变化如何影响平衡位置。

1. 浓度的变化: - 增加反应物浓度(N₂ 或 H₂): 如果向体系中加入氮气(N₂)或氢气(H₂),这些反应物的浓度会增加。根据勒夏特列原理,平衡会朝消耗这些额外反应物的方向移动,即向右移动,有利于生成氨(NH₃)。

  • 减少产物浓度(NH₃): 如果从体系中移除氨,NH₃ 的浓度会降低。平衡将向右移动以补偿这种降低,再次有利于氨的生成。

  • 增加产物浓度(NH₃): 如果向体系中加入氨,平衡将向左移动以减少 NH₃ 的过量,有利于生成 N₂ 和 H₂。

2. 压力的变化:

  • 系统总压增大: 对于气体,压力增大(通过减少体积)有利于产生气体摩尔数较少的反应。在此反应中,从 4 摩尔气体(1 N₂ + 3 H₂)变为 2 摩尔气体(2 NH₃)。因此,增压使平衡向右移动,有利于氨的形成。

  • 系统总压降低: 相反,压力降低会有利于产生更多气体摩尔数的反应,使平衡向左移动,并促进 NH₃ 解离为 N₂ 和 H₂。

3. 温度的变化:

  • 反应的放热性质: 合成氨是放热反应(ΔH<0\Delta H < 0),即放出热量。

  • 温度升高: 提高温度会向体系提供热量。根据勒夏特列原理,平衡会朝吸收这部分额外热量的方向移动,因此向左(吸热反应)移动,促进 NH₃ 解离为 N₂ 和 H₂。

  • 温度降低: 降低温度时,体系会通过生成热量来补偿这种损失。平衡因此向右(放热反应)移动,有利于氨的形成。

4. 催化剂的存在:

  • 催化剂的作用: 添加催化剂(例如经助催化剂处理的细粉铁)可以加快达到平衡的速率,但不会改变平衡位置。它对正反应和逆反应的加速作用是相等的。

总结:

勒夏特列原理可以预测化学平衡在系统条件改变时的移动方向。对于合成氨:

  • 高压 有利于 NH₃ 的生成(气体摩尔数较少)。
  • 低温 有利于 NH₃ 的生成(放热反应)。
  • 增加 N₂ 或 H₂ 的浓度 有利于 NH₃ 的生成。
  • 从体系中移除 NH₃ 有利于生成更多的 NH₃。

这些原理在哈伯-博世(Haber-Bosch)工艺中得到应用,以优化工业上氨的生产。

结论

该 Python 脚本为 OpenAI o1-preview 模型提供了实用的集成,使其先进的推理能力可用于从简单到复杂的各种问题。无论您是好奇的初学者,还是希望测试模型极限的专家,这个脚本都是您人工智能项目的一个不错的工具。

资源

本文件已使用模型 gpt-5-mini 将 fr 版本翻译为 zh 语言。有关翻译过程的更多信息,请参阅 https://gitlab.com/jls42/ai-powered-markdown-translator