abshoff.dev: Web, Mobile & Cloud!

Migration von WordPress zu Hugo: Markdown mit OpenAI-API

Umzugskartons

In einem zuvor erschienenen Artikel habe ich gezeigt, wie ich Beiträge im HTML-Format aus einer WordPress-Installation extrahieren kann. In diesem Artikel zeige ich Dir, wie Du die HTML-Dateien in Markdown-Dateien umwandeln kannst. Dazu verwende ich die OpenAI-API, also die programmierbare Version von ChatGPT.

Markdown ist eine vereinfachte Auszeichnungssprache und wird von Hugo für die Textformatierung verwendet. Markdown ist weniger mächtig als HTML. Daher ist die Umwandlung von HTML zu Markdown nicht trivial. Ein Werkzeug, mit dem man HTML in Markdown umwandeln kann, ist Pandoc. Weil die vorhandenen Beiträge sehr uneinheitliche HTML-Strukturen aufweisen, habe ich mich entschieden, für die Umwandlung die OpenAI-API auszuprobieren. Diese ist zwar nicht kostenlos, aber die Ergebnisse sind sehr brauchbar.

OpenAI-API

Die OpenAI-API ermöglicht die Nutzung von AI-Modellen über eine Programmierschnittstelle. Das Chat-Complete-Modell der API ist ein Sprachmodell, das auf der Basis von Texteingaben passende Textausgaben erzeugt. Über Nachrichten mit unterschiedlichen Rollen kann das Modell gesteuert und parametrisiert werden. Die Nachrichten werden im JSON-Format an die API gesendet. Die API antwortet mit einer JSON-Nachricht, die die Textausgabe enthält.

In der System-Rolle teile ich dem Modell mit, was die Nutzer-Eingabe und was seine Aufgabe sein wird:

Du erhältst als Eingabe HTML-Code. Deine Aufgabe ist es, diesen in die nächstliegende Markdown-Darstellung umzuwandeln. Entferne vor der Umwandlung alle HTML-Kommentare in der Eingabe. Ein Bild “test.jpg” sollte als “![Ein Testbild](test.jpg)” dargestellt werden, wobei “Ein Testbild” das alt- oder titel-Attribut und “test.jpg” das src-Attribut ist.

Die Nutzer-Eingabe ist dann einfach nur der HTML-Code, der umgewandelt werden soll. Nachfolgendes Deno-Skript mit dem Namen convert-to-markdown-with-openai.js nutzt das NPM-OpenAI-Paket und das Chat-Complete-Modell der OpenAI-API, um HTML-Code in Markdown umzuwandeln:

import { existsSync } from "https://deno.land/std/fs/mod.ts";
import OpenAI from 'npm:openai@latest';

function* walkDirectory(start, path) { // das ist eine nützliche rekursive Generator-Funktion :-)
    for (const entry of Deno.readDirSync(`${start}/${path ?? ''}`)) {
        const entryPath = `${path && path.length > 0 ? `${path}/` : ''}${entry.name}`;
        if (entry.isFile) {
            yield entryPath;
        }
        if (entry.isDirectory) {
            yield* walkDirectory(start, entryPath);
        }
    }
}

const openai = new OpenAI({
    apiKey: Deno.env.get('OPENAI_API_KEY'),
});

async function convertToMarkdown(html) {
    const chatCompletion = await openai.chat.completions.create({
        messages: [
            {
                role: 'system',
                content: 'You are provided with HTML code. Your task is to transform it to its closest Markdown representation. Before conversion, remove any HTML comments in the input. An image "test.jpg" should be represented as "![A test image](test.jpg)" where "A test image" is the alt or title attribute and "test.jpg" is the src attribute.',
            },
            {
                role: 'user',
                content: html,
            }
        ],
        model: 'gpt-3.5-turbo',
    });
    console.log({ chatCompletion, });
    return chatCompletion.choices[0].message.content;
};

const htmlPath = 'out/html';
const markdownPath = 'out/md';

for (const filePath of walkDirectory(htmlPath)) {
    if (!filePath.endsWith('.html')) {
        continue;
    }

    const filename = `${htmlPath}/${filePath}`;
    const newFilename = `${markdownPath}/${filePath.replace(/\.html/, '.md')}`;

    if (existsSync(newFilename)) {
        continue;
    }

    const contents = await Deno.readTextFile(filename);

    console.log({
        input: filename,
        contents,
    });

    const newContents = await convertToMarkdown(contents);

    console.log(newContents);

    await Deno.writeTextFile(newFilename, newContents);
}

Bei mir liegen die HTML-Dateien im Ordner out/html und die Markdown-Dateien sollen im Ordner out/md abgelegt werden. Das Skript kann über den folgenden Befehl gestartet werden, wobei der OpenAI-API-Key über die Umgebungsvariable OPENAI_API_KEY übergeben werden muss:

OPENAI_API_KEY="" deno run --allow-read=./out --allow-write=./out/md --allow-env=OPENAI_API_KEY --allow-net=api.openai.com convert-to-markdown-with-openai.js

Kosten für die Nutzung der OpenAI-API

Die Abrechnung hängt von der Anzahl der Tokens in der Ein- und Ausgabe ab. Ein Token entspricht etwa 0,75 Wörtern in einem englischen Text. In meinem Beispiel habe ich 82 HTML-Dateien mit insgesamt 87078 Tokens mit dem Modell gpt-3.5-turbo-0613 für insgesamt 0,25$ umgewandelt. Das entspricht etwa 0,0003$ pro Datei. Die Kosten sind also sehr gering.

Beispielkonvertierung

Ich habe folgende Beispiel-HTML-Datei als Eingabe:

<h1>Lorem Ipsum</h1>

<div id="navigation">
    <ul>
        <li><a href="#abschnitt1">Abschnitt 1</a></li>
        <li><a href="#abschnitt2">Abschnitt 2</a>
            <ul>
                <li><a href="#abschnitt2a">Teil A</a></li>
                <li><a href="#abschnitt2b">Teil B</a></li>
            </ul>
        </li>
        <li><a href="#abschnitt3">Abschnitt 3</a></li>
    </ul>
</div>

<div id="content">
    <div id="abschnitt1">
        <h2>Abschnitt 1</h2>
        <p><strong>Lorem</strong> ipsum dolor sit amet.</p>
        <img src="bild1.jpg" alt="Bild 1">
        <ul>
            <li>Punkt 1</li>
            <li>Punkt 2
                <ul>
                    <li>Unterpunkt 2a</li>
                    <li>Unterpunkt 2b</li>
                </ul>
            </li>
        </ul>
    </div>

    <div id="abschnitt2">
        <h2>Abschnitt 2</h2>
        <div id="abschnitt2a">
            <h3>Teil A</h3>
            <p><strong>Sed</strong> cursus ante dapibus diam.</p>
            <img src="bild2.jpg" alt="Bild 2">
        </div>
        <div id="abschnitt2b">
            <h3>Teil B</h3>
            <table>
                <tr>
                    <th>Spalte 1</th>
                    <th>Spalte 2</th>
                </tr>
                <tr>
                    <td><img src="bild3.jpg" alt="Bild 3"></td>
                    <td><strong>Daten 2</strong></td>
                </tr>
            </table>
        </div>
    </div>

    <div id="abschnitt3">
        <h2>Abschnitt 3</h2>
        <p>Nullam quis <strong>risus</strong> eget urna mollis ornare.</p>
        <ol>
            <li>Erster Schritt</li>
            <li>Zweiter Schritt
                <img src="bild4.jpg" alt="Bild 4">
            </li>
        </ol>
    </div>
</div>

Das OpenAI-Skript generiert daraus folgende Markdown-Ausgabe:

# Lorem Ipsum

## Abschnitt 1
**Lorem** ipsum dolor sit amet.
![Bild 1](bild1.jpg)
- Punkt 1
- Punkt 2
  - Unterpunkt 2a
  - Unterpunkt 2b

## Abschnitt 2

### Teil A
**Sed** cursus ante dapibus diam.
![Bild 2](bild2.jpg)

### Teil B

| Spalte 1 | Spalte 2 |
|---------|---------|
| ![Bild 3](bild3.jpg) | **Daten 2** |

## Abschnitt 3
Nullam quis **risus** eget urna mollis ornare.
1. Erster Schritt
2. Zweiter Schritt ![Bild 4](bild4.jpg)

Die Eingabe enthält 638 Tokens und die Ausgabe 172 Tokens. Die Kosten dafür belaufen sich auf weniger als 0,01$.

Tags: