Приключения вайбкодера в Ворде, или Как две нейронки не осилили простенькую задачку

—

от автора

Проблема

Мой коллега, физик-теоретик, попросил меня помочь с оформлением своих научных отчетов и статей. Пишет он эти документы в обычном Ворде с использованием программы под названием MathType — это, по-простоиму, такой визуальный LaTeX для написания уравнений, штука достаточно удобная, а главное — она сама и номера уравнений вставляет и чертовски здорово оперирует с ними, даже если их переносишь из одного документа в другой.

Проблема возникла, когда он начал использовать нейронку (Copilot) для “вычистки” своих статей: если статья написана так, что и нейронка ее хорошо понимает, значит, есть шанс, что и дураки белковые собратья поймут. Увы — нейронки не очень понимают этот самый MathType. Приходится переводить формулы в обычный текстовый LaTeX (MathType это делает легко и просто, одним кликом во всем документе), потом скармливать сей нечитаемый (для человека) текст нейронке, что-то с ним делать, а потом снова конвертировать формулы в MathType. Он это тоже умеет, но… только по одной формуле за раз; почему — неведомо. А что, если этих формул там больше тыщи? При том, что мой коллега тот еще любитель каллиграфии и даже выражения типа “икс в квадрате” набирает через MathType, это та еще заморочка…

То бишь, надо было автоматизировать процедуру конверсии математических формул во всем вордовском документе из LaTeX’a в MathType.

Первые пробы

Что до меня — в Экселе я работаю много и по-разному, это прекрасный инструмент для прототипирования и проверки мат. Моделей (в том числе и весьма сложных) на работоспособность, с готовым визуальным интерфейсом, в котором можно посмотреть, в принципе, все, что угодно, плюс замечательная возможность правки кода прямо во время выполнения, не сильно теряя при этом в скорости счета. Но Ворд — это зверь малость другой породы: там тоже VBA, но малость другой. Примерно как микролитражка и автобус — это, по сути, две “легковушки”, но вождение чуток разное.

Тем не менее, задача не казалась сложной (и, забегая вперед, она в самом деле оказалась простенькой), и я сказал: окей, попробую.

Понятно, чтобы не мучиться, решил прибегнуть к помощи нейронки. Использовал две: Microsoft Copilot, у которого под капотом OpenAI (безлимит, агент встроен прямо в Ворд), и Google Gemini (из браузера, бесплатный). УВы, ни тот, ни другой (и ни третий — который доступен через стандартный агентский интерфейс) не могут сами влезть в Ворд и смастерить макруху — то ли не хватает им привилегий, то ли мне не хватило знаний, как запустить их внутрь вордовского VBA. Пришлось консультироваться, так сказать, дистанционно.

Начал, понятное дело, с Copilot — вроде как он свой, майкрософтовский, должен знать офисный софт от и до. Может быть, даже и знает… Но MathType — это все же не родная офисная тулза, а внешняя библиотека. Агент про нее вполне себе осведомлен, но напрямую с ней работать не может. Первое, что он мне сказал, — что ему было бы проще, если бы формулы были не в MathType-е, а в родном офисовском формате. И даже набросал макрос, который этим занимается.

Макрос, правда, оказался нерабочим. Но худо-бедно я смог его починить. Для начала было неплохо. Но я все же настаивал на MathType… как потом выяснилось, зря я это делал, но — задним умом все сильны. В итоге, после нескольких итераций, мы с ним даже сделали макрос, который таки сам переводил LaTeX в MathType. Вот только работал он… ммм… не всегда. Контрольную задачку “икс равно минус а плюс-минус корень из бэ-квадрат минус четыре а цэ пополам” он в итоге решил, но только формулы у коллеги были малость посложнее — и с ними он справиться почему-то не мог…

Я обратился к Гуглу. Говорю, так и так, есть такая задача, нужно сделать вот это. Он сказал — нет проблем, и выдал макруху. Естественно, нерабочую. После серии “переговоров”, в которых он каждый раз предлагал изменение, которое обязательно должно сработать (и подробно объяснял, почему), но на самом деле почему-то не работало, я на это плюнул — и начал ваять код руками…

Ручная работа

К моему удивлению (после всех этих мытарств), решение оказалось и в самом деле очень простым. Надо было только три вещи:

  1. Точно (и, главное, грамотно) отыскать все латексные формулы в документе.

  2. Преобразовать каждую формулу в родное офисное уравнение.

  3. Из офисного формата преобразовать формулы в MathType. 

Вторую из этих операций мы с агентом уже освоили в ходе наших мучений, а третья — делается в один клик самим MathType.

Оставалось, таким образом, сделать только первый пункт — искалку для латексных формул в тексте вордовского документа. После пары часов проб и ошибок, с задачей я справился. См. процедуру wd_ConvertAllLatex2EqObjects в коде ниже. Только, пожалуйста, не бросайте в меня гнилыми помидорами за качество кода… Хоть эта процедура и не вайбленая, но вылизывать ее у меня не было ни времени, ни желания. Она работает, в том числе и на тыще с лишним формул разной степени мерзости сложности, а больше мне от нее ничего не надо, продавать ее никому не собираюсь.

Снова к нейронке

Оставалось сделать последний шажок: встроить процедурину непосредственно в вордовский интерфейс, чтобы в код не ходить. И здесь, надо отдать должное Гуглу, он если не с самой первой, то со второй попытки предложил решение, которое меня уже в принципе устроило: макрос на автозапуск файла, и после этого в контекстном меню появляется дополнительная строчка, напрямую вызывающая макрос. Еще немного игр с бубном, и этот макрос заработал в любом открытом документе. Заняло это от силы полчаса. После отработки макроса документ, в принципе, можно использовать и так — офисные формулы от маттайповских на вид неотличимы. А если хочется все-таки в MathType — делается это одним кликом.

Всё. Задача решена.

Итог

Код привожу ниже. Опять же, частично он навайбленный, частично мой, но написан на коленке. Уверен, что ему далеко до оптимальности, и спецы по автоматизации ворда легко сделали бы его нормальным, и, скорее всего, и само решение было бы совсем не таким. Но, повторюсь, это не коммерческий продукт, не корпоративный, и даже не пет-проект — конкретная “мулька” для конкретного человека. Если кому-то она тоже пригодится — буду рад.

Что надо сделать:

  1. Открыть в ворде пустой документ.

  2. Перейти в редактор кода по Alt+F11. 

  3. Вставить в код документа VBA-модуль. Записать в него первый фрагмент кода.

  4. В код ThisDocument вставить второй фрагмент.

  5. Файл записать с расширением .dotm — шаблон с макросами.

  6. Сохраненный файл скопировать в фолдер, в котором хранятся файлы самого ворда (это выяснить очень легко и многими путями).

  7. Открыть документ с латексными формулами.

  8. Нажать правую кнопку и из контекстного меню выбрать образовавшийся нижний пункт.

Возможно, не у всех заработает, с виндой это часто бывает, прошу прощения заранее.

Вот первый фрагмент кода:

Скрытый текст
Option ExplicitDefLng I-N, QDefSng XDefStr SDefDbl DDefBool B#If VBA7 Then    ' 64-bit Windows API Declarations for Clipboard Management    Private Declare PtrSafe Function OpenClipboard Lib "user32" (ByVal hWnd As LongPtr) As Long    Private Declare PtrSafe Function CloseClipboard Lib "user32" () As Long    Private Declare PtrSafe Function EmptyClipboard Lib "user32" () As Long    Private Declare PtrSafe Function SetClipboardData Lib "user32" (ByVal wFormat As Long, ByVal hMem As LongPtr) As LongPtr    Private Declare PtrSafe Function GlobalAlloc Lib "kernel32" (ByVal wFlags As Long, ByVal dwBytes As LongPtr) As LongPtr    Private Declare PtrSafe Function GlobalLock Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr    Private Declare PtrSafe Function GlobalUnlock Lib "kernel32" (ByVal hMem As LongPtr) As Long    Private Declare PtrSafe Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal Destination As LongPtr, ByVal Source As String, ByVal Length As LongPtr)#Else    ' 32-bit Legacy Declarations (for backwards compatibility)    Private Declare Function OpenClipboard Lib "user32" (ByVal hwnd As Long) As Long    Private Declare Function CloseClipboard Lib "user32" () As Long    Private Declare Function EmptyClipboard Lib "user32" () As Long    Private Declare Function SetClipboardData Lib "user32" (ByVal wFormat As Long, ByVal hMem As Long) As Long    Private Declare Function GlobalAlloc Lib "kernel32" (ByVal wFlags As Long, ByVal dwBytes As Long) As Long    Private Declare Function GlobalLock Lib "kernel32" (ByVal hMem As Long) As Long    Private Declare Function GlobalUnlock Lib "kernel32" (ByVal hMem As Long) As Long    Private Declare Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As String, ByVal Length As Long)#End IfPrivate Const GMEM_MOVEABLE As Long = &H2Private Const CF_TEXT As Long = 1' ==============================================================================' MAIN AUTOMATION ENTRY POINT' ==============================================================================Public Sub wd_ConvertAllLatex2EqObjects()  Dim Doc As Document  Dim Sel As Selection  Dim Prg As Word.Paragraph    Dim sEq As String, sWd As String  Dim sStart, sEnd  Dim iPstart, iPend, iEqCountInline, iEqCountCentered, iPrg  Dim bStarted, bEnded    iPstart = 0  Set Doc = ActiveDocument  Set Sel = Selection  bStarted = False  bEnded = False  sEnd = ""    '======================  ' Inline equations  '======================  sStart = "$"  sEnd = "$"  Sel.HomeKey wdStory  iEqCountInline = 0  Do    Do      ' Find starting delimiter      SetSelectionFind sStart, bWildcards:=False      If Not Sel.Find.Execute() Then        Exit Do      End If            Set Prg = Sel.Paragraphs(1)      iPstart = Sel.Start      ' Find ending delimiter      Sel.MoveRight wdCharacter      SetSelectionFind sEnd, bWildcards:=False      If Not Sel.Find.Execute() Then        Exit Do      End If      If Sel.End > Prg.Range.End Then        ' second delimiter out of paragraph        Sel.EndKey wdLine        Exit Do      End If      iPend = Sel.End            ' Convert content inside delimiters      Doc.Range(iPstart, iPend).Select      ConvertSelectedLaTeXToOfficeEquation sStart, sEnd      With Sel        .MoveRight wdCharacter      End With      iEqCountInline = iEqCountInline + 1    Loop    If Sel.Paragraphs(1).Range.End >= Doc.Range.End - 1 Then      Exit Do    End If    Sel.EndKey wdLine    Sel.MoveDown wdParagraph  Loop    '======================  ' Centered equations  '======================  sStart = "\["  sEnd = "\]"  Sel.HomeKey wdStory  iEqCountCentered = 0  Do    ' Find starting delimiter    SetSelectionFind sStart, bWildcards:=False    If Not Sel.Find.Execute() Then      Exit Do    End If        iPstart = Sel.Start        ' Find ending delimiter    Sel.MoveRight wdCharacter    SetSelectionFind sEnd, bWildcards:=False    If Not Sel.Find.Execute() Then      Exit Do    End If    iPend = Sel.End        ' Convert content inside delimiters    Doc.Range(iPstart, iPend).Select    ConvertSelectedLaTeXToOfficeEquation sStart, sEnd    With Sel      .MoveRight wdCharacter      .Delete wdCharacter      .HomeKey wdLine      .MoveLeft wdCharacter      .TypeText Text:=vbTab      .ParagraphFormat.Alignment = wdAlignParagraphJustify 'wdAlignParagraphCenter      .HomeKey wdLine      .MoveLeft wdCharacter      .Delete      .MoveLeft wdCharacter      .Delete      .MoveDown wdParagraph    End With    iEqCountCentered = iEqCountCentered + 1  Loop    ' Finish  MsgBox CStr(iEqCountCentered) & " right-numbered and " _    & CStr(iEqCountInline) & " inline equation(s) converted"  pExit:  Set Sel = Nothing  Set Doc = Nothing  Set Prg = NothingEnd Sub' ==============================================================================' PRIVATE HELPER ROUTINES & API INTERFACES' ==============================================================================Private Sub ConvertSelectedLaTeXToOfficeEquation _  (Optional Start As String = "\[", Optional sEnd As String = "\]")    Dim latexString As String    Dim originalRange As Range    Dim insertRange As Range    If Selection.Range.Start = Selection.Range.End Then        MsgBox "Please select a LaTeX expression.", vbExclamation        Exit Sub    End If    Set originalRange = Selection.Range.Duplicate    latexString = Trim$(originalRange.Text)    ' Remove paragraph or table-cell terminators.    If Right$(latexString, 2) = Chr$(13) & Chr$(7) Then        latexString = Left$(latexString, Len(latexString) - 2)    ElseIf Right$(latexString, 1) = Chr$(13) Then        latexString = Left$(latexString, Len(latexString) - 1)    End If    ' Add display-math delimiters only when they are absent.    If Left$(latexString, Len(Start)) <> Start Or Right$(latexString, Len(sEnd)) <> sEnd Then        latexString = Start & latexString & sEnd    End If    CopyToClipboardAPI latexString        Wait    Selection.PasteEnd SubPrivate Sub CopyToClipboardAPI(ByVal textToCopy As String)    Dim hGlobalMemory As LongPtr    Dim lpGlobalMemory As LongPtr        ' Allocate moveable memory for the string    hGlobalMemory = GlobalAlloc(GMEM_MOVEABLE, Len(textToCopy) + 1)    If hGlobalMemory = 0 Then Exit Sub        ' Lock the block and copy string into it    lpGlobalMemory = GlobalLock(hGlobalMemory)    If lpGlobalMemory <> 0 Then        MoveMemory lpGlobalMemory, textToCopy, Len(textToCopy) + 1        GlobalUnlock hGlobalMemory                ' Open clipboard and pass the data handle        If OpenClipboard(0) <> 0 Then            EmptyClipboard            SetClipboardData CF_TEXT, hGlobalMemory            CloseClipboard        End If    End IfEnd SubPrivate Sub Wait(Optional xTimeSec = 0.1)  Dim pauseTime As Single  pauseTime = Timer() + xTimeSec    ' Midnight rollover protection checking if timer overflows past 24 hours  If pauseTime >= 86400 Then pauseTime = pauseTime - 86400    Do While Timer() < pauseTime      DoEvents  LoopEnd SubPrivate Sub SetSelectionFind( _  sFind As String, _  Optional sReplace As String = "", _  Optional bStop As Boolean = True, _  Optional bForward As Boolean = True, _  Optional bWholeWord As Boolean = False, _  Optional bWildcards As Boolean = True _)  With Selection.Find    .ClearFormatting    .Text = sFind    .Replacement.Text = sReplace    .Forward = bForward    If bStop Then      .Wrap = wdFindStop    Else      .Wrap = wdFindContinue    End If    .Format = False    .MatchByte = False    .MatchWildcards = bWildcards    .MatchAllWordForms = False    .MatchWholeWord = bWholeWord  End WithEnd Sub

А вот второй фрагмент:

Скрытый текст
Option Explicit' ==============================================================================' GLOBAL AUTOEXEC ADD-IN RIBBON INITIALIZATION' ==============================================================================Public Sub Autoexec()    Dim contextMenu As CommandBar    Dim customButton As CommandBarButton        Set contextMenu = Application.CommandBars("Text")        On Error Resume Next    contextMenu.Controls("LaTeX -> Equations").Delete    On Error GoTo 0        Set customButton = contextMenu.Controls.Add(Type:=msoControlButton, Temporary:=True)    With customButton        .Caption = "LaTeX -> Equations"        .OnAction = "wd_ConvertAllLatex2EqObjects"        .FaceId = 353     End WithEnd Sub

Так в чем же была сложность?

Если по-честному — проблема была, как водится, в постановке исходной задачи: одним кликом перевести все формулы в документе из латекса в MathType. Ибо кликов для этого понадобилось не один, а два. А еще надо было понять, как разделить задачу на части (да-да, задним умом все сильны!), ну и, конечно, написать ту самую простенькую искалку на сотню VBA-шных строк — задача, думаю, и нейронке тоже посильная… если ее правильно попросить.

Увы, сделать этот переход — решить задачу не в один клик, а в два, — нейронке, даже двум, оказалось не по силам. Для этого понадобилась “мясная прокладка” в виде вашего покорного слуги.

К сожалению, модифицировать постановку задачи, исходя из особенностей реальной ситуации (для моего коллеги большой разницы между одним и двумя-тремя кликами нет никакой), — это то, в чем наши кремниевые коллеги пока что не сильно справляются. И это, думаю, то, что неплохо понимать всем, кто сталкивается с разнообразными вайбкодными проблемами: если решение не получается найти — значит, весьма вероятно, что-то не то в постановке задачи. А это — то, что нейронки пока что, увы, сами обнаружить не в состоянии.

 Всем успехов — с вайбкодингом и без!

ссылка на оригинал статьи https://habr.com/ru/articles/1095958/