Приключения вайбкодера в Ворде, или Как две нейронки не осилили простенькую задачку
Мой коллега, физик-теоретик, попросил меня помочь с оформлением своих научных отчетов и статей. Пишет он эти документы в обычном Ворде с использованием программы под названием MathType - это, по-простоиму, такой визуальный LaTeX для написания уравнений, штука достаточно удобная, а главное - она сама и номера уравнений вставляет и чертовски здорово оперирует с ними, даже если их переносишь из одного документа в другой.
Проблема возникла, когда он начал использовать нейронку (Copilot) для “вычистки” своих статей: если статья написана так, что и нейронка ее хорошо понимает, значит, есть шанс, что и дураки белковые собратья поймут. Увы - нейронки не очень понимают этот самый MathType. Приходится переводить формулы в обычный текстовый LaTeX (MathType это делает легко и просто, одним кликом во всем документе), потом скармливать сей нечитаемый (для человека) текст нейронке, что-то с ним делать, а потом снова конвертировать формулы в MathType. Он это тоже умеет, но… только по одной формуле за раз; почему - неведомо. А что, если этих формул там больше тыщи? При том, что мой коллега тот еще любитель каллиграфии и даже выражения типа “икс в квадрате” набирает через MathType, это та еще заморочка…
То бишь, надо было автоматизировать процедуру конверсии математических формул во всем вордовском документе из LaTeX’a в MathType.
Первые пробы
Что до меня - в Экселе я работаю много и по-разному, это прекрасный инструмент для прототипирования и проверки мат. Моделей (в том числе и весьма сложных) на работоспособность, с готовым визуальным интерфейсом, в котором можно посмотреть, в принципе, все, что угодно, плюс замечательная возможность правки кода прямо во время выполнения, не сильно теряя при этом в скорости счета. Но Ворд - это зверь малость другой породы: там тоже VBA, но малость другой. Примерно как микролитражка и автобус - это, по сути, две “легковушки”, но вождение чуток разное.
Тем не менее, задача не казалась сложной (и, забегая вперед, она в самом деле оказалась простенькой), и я сказал: окей, попробую.
Понятно, чтобы не мучиться, решил прибегнуть к помощи нейронки. Использовал две: Microsoft Copilot, у которого под капотом OpenAI (безлимит, агент встроен прямо в Ворд), и Google Gemini (из браузера, бесплатный). УВы, ни тот, ни другой (и ни третий - который доступен через стандартный агентский интерфейс) не могут сами влезть в Ворд и смастерить макруху - то ли не хватает им привилегий, то ли мне не хватило знаний, как запустить их внутрь вордовского VBA. Пришлось консультироваться, так сказать, дистанционно.
Начал, понятное дело, с Copilot - вроде как он свой, майкрософтовский, должен знать офисный софт от и до. Может быть, даже и знает… Но MathType - это все же не родная офисная тулза, а внешняя библиотека. Агент про нее вполне себе осведомлен, но напрямую с ней работать не может. Первое, что он мне сказал, - что ему было бы проще, если бы формулы были не в MathType-е, а в родном офисовском формате. И даже набросал макрос, который этим занимается.
Макрос, правда, оказался нерабочим. Но худо-бедно я смог его починить. Для начала было неплохо. Но я все же настаивал на MathType… как потом выяснилось, зря я это делал, но - задним умом все сильны. В итоге, после нескольких итераций, мы с ним даже сделали макрос, который таки сам переводил LaTeX в MathType. Вот только работал он… ммм… не всегда. Контрольную задачку “икс равно минус а плюс-минус корень из бэ-квадрат минус четыре а цэ пополам” он в итоге решил, но только формулы у коллеги были малость посложнее - и с ними он справиться почему-то не мог…
Я обратился к Гуглу. Говорю, так и так, есть такая задача, нужно сделать вот это. Он сказал - нет проблем, и выдал макруху. Естественно, нерабочую. После серии “переговоров”, в которых он каждый раз предлагал изменение, которое обязательно должно сработать (и подробно объяснял, почему), но на самом деле почему-то не работало, я на это плюнул - и начал ваять код руками…
Ручная работа
К моему удивлению (после всех этих мытарств), решение оказалось и в самом деле очень простым. Надо было только три вещи:
Точно (и, главное, грамотно) отыскать все латексные формулы в документе.
Преобразовать каждую формулу в родное офисное уравнение.
Из офисного формата преобразовать формулы в MathType.
Вторую из этих операций мы с агентом уже освоили в ходе наших мучений, а третья - делается в один клик самим MathType.
Оставалось, таким образом, сделать только первый пункт - искалку для латексных формул в тексте вордовского документа. После пары часов проб и ошибок, с задачей я справился. См. процедуру wd_ConvertAllLatex2EqObjects в коде ниже. Только, пожалуйста, не бросайте в меня гнилыми помидорами за качество кода… Хоть эта процедура и не вайбленая, но вылизывать ее у меня не было ни времени, ни желания. Она работает, в том числе и на тыще с лишним формул разной степени мерзости сложности, а больше мне от нее ничего не надо, продавать ее никому не собираюсь.
Снова к нейронке
Оставалось сделать последний шажок: встроить процедурину непосредственно в вордовский интерфейс, чтобы в код не ходить. И здесь, надо отдать должное Гуглу, он если не с самой первой, то со второй попытки предложил решение, которое меня уже в принципе устроило: макрос на автозапуск файла, и после этого в контекстном меню появляется дополнительная строчка, напрямую вызывающая макрос. Еще немного игр с бубном, и этот макрос заработал в любом открытом документе. Заняло это от силы полчаса. После отработки макроса документ, в принципе, можно использовать и так - офисные формулы от маттайповских на вид неотличимы. А если хочется все-таки в MathType - делается это одним кликом.
Всё. Задача решена.
Итог
Код привожу ниже. Опять же, частично он навайбленный, частично мой, но написан на коленке. Уверен, что ему далеко до оптимальности, и спецы по автоматизации ворда легко сделали бы его нормальным, и, скорее всего, и само решение было бы совсем не таким. Но, повторюсь, это не коммерческий продукт, не корпоративный, и даже не пет-проект - конкретная “мулька” для конкретного человека. Если кому-то она тоже пригодится - буду рад.
Что надо сделать:
Открыть в ворде пустой документ.
Перейти в редактор кода по Alt+F11.
Вставить в код документа VBA-модуль. Записать в него первый фрагмент кода.
В код ThisDocument вставить второй фрагмент.
Файл записать с расширением .dotm - шаблон с макросами.
Сохраненный файл скопировать в фолдер, в котором хранятся файлы самого ворда (это выяснить очень легко и многими путями).
Открыть документ с латексными формулами.
Нажать правую кнопку и из контекстного меню выбрать образовавшийся нижний пункт.
Возможно, не у всех заработает, с виндой это часто бывает, прошу прощения заранее.
Вот первый фрагмент кода:
Скрытый текст
Option Explicit
DefLng I-N, Q
DefSng X
DefStr S
DefDbl D
DefBool 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 If
Private Const GMEM_MOVEABLE As Long = &H2
Private 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 = Nothing
End 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.Paste
End Sub
Private 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 If
End Sub
Private 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
Loop
End Sub
Private 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 With
End 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 With
End Sub
Так в чем же была сложность?
Если по-честному - проблема была, как водится, в постановке исходной задачи: одним кликом перевести все формулы в документе из латекса в MathType. Ибо кликов для этого понадобилось не один, а два. А еще надо было понять, как разделить задачу на части (да-да, задним умом все сильны!), ну и, конечно, написать ту самую простенькую искалку на сотню VBA-шных строк - задача, думаю, и нейронке тоже посильная… если ее правильно попросить.
Увы, сделать этот переход - решить задачу не в один клик, а в два, - нейронке, даже двум, оказалось не по силам. Для этого понадобилась “мясная прокладка” в виде вашего покорного слуги.
К сожалению, модифицировать постановку задачи, исходя из особенностей реальной ситуации (для моего коллеги большой разницы между одним и двумя-тремя кликами нет никакой), - это то, в чем наши кремниевые коллеги пока что не сильно справляются. И это, думаю, то, что неплохо понимать всем, кто сталкивается с разнообразными вайбкодными проблемами: если решение не получается найти - значит, весьма вероятно, что-то не то в постановке задачи. А это - то, что нейронки пока что, увы, сами обнаружить не в состоянии.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.