Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Google Audio Book Creator

5.00/5 (2 votes)
25 Jul 2024CPOL5 min read 3.1K   212  
Free Audio Book Creator using Google Text-to-Speech AI API
This application lets you create an audio book using Google TTS API. The application will generate mp3 file for each paragraph and then merge them together.

Introduction

This application lets you create an audio book using Google Text-to-Speech AI API. The application will generate mp3 file for each paragraph and then merge them together.  The Google Text-to-Speech AI is $16 per 1 million characters. First 1 million characters FREE.  An average 200 page audiobook is about 0.2 million characters.  Which means that you can create 5 audiobooks a month for free. 

In contrast, OpenAI Text-to-Speech API is $30.00 per million characters.  And Eleven Labs is $99 per half a million characters.

In addition Google Text-to-Speech lets you specify the language and the speech in non-English language is without accent.  In contrast, OpenAI and Eleven Labs have bad foreign accent for non-English languages.

If you have a pdf file, the text file needs to be generated by opening the pdf file in Word and copied and pasted to a text file. Open the text file in a text editor that has line numbers such as Notepad2. Open the file in and remove text that would not be read like: table of contents, footnotes, index and references. Each paragraph must be on one line.  The Application has AI Proofreading section so this work can be automated by using OpenAI or Anthropic.

There is a 0.3 second pause between paragraphs. One blank line means 1 second pause. Two blank lines means new Chapter.

One blank line means 1 second pause. But you can use the “Silence” feature to customize the pause duration.

Image 1

Background

This article is a sequel to two my other articles I wrote about creating AI  audiobooks using Eleven Labs and OpenAI TTS.

How to Create an Audiobook

  • First get API Key from Google
  • Select Language and Voice. 
  • “Say it” will generate mp3 file based on any text and place the file into Temp folder in the same folder as the EXE.
  • “Highlight text when” option helps you with text file correction and editing before to generate the mp3 files. For example, “Begins with lower case character” and “Begins with a number” option will highlight paragraphs that might be broken during Word PDF conversion. “Contains number” might help identify paragraphs that contain a footnote number.
  • “Save text file” saved the changes in the text file. “Backup text file” option creates a backup to let you undo the changes you made to the text file. The backup text files will be placed in the folder with the same name as the text file name plus “_backup”.
  • 1. Process Text File” will generate MP3 file for each line in the text file. The file will be placed in the folder with the same name as the text file name. Each file will be named after the line number in the text file like 0001.mp3. This means that you should not add or delete lines to the text file after MP3 files are generated.
  • Select a line in the grid and click Play to play an MP3 file. Click Stop to stop the mp3 file playing. Select a line in the grid and click Delete to delete an MP3 file.
  • You can delete bad mp3 files and click 1. Process Text File again to regenerate the mp3 files that were deleted.
  • You can also select a line in the grid and click Regenerate to re-create the MP3 file. This option will also save the text file if needed.
  • Play on key up option allows you to listen to the entire book by pressing the arrow down key after selecting a line text.
  • Once you are satisfied with the quality of the generated mp3 files, click 2. MP3 Chapters to generate mp3 file for each chapter. Two blank lines in the text file means new Chapter. The files will be placed in the folder with the same name as the text file name plus -Chapters.
  • Merge MP3 Chapter files into one mp3 file. The mp3 file will be placed in the folder as the text file and have the same name but with mp3 extension.

To add figure image file for each paraph:

  • Create an "images" folder.  This will show "Figures" panel  on the form.  
  • Create "Figure" folder inside of the "images" folder. Place image files like: 1-1.png.
  • If any paragraph text will reference "Figure 1-1", it will include 1-1.png in the video file. 
  • Create "Table" folder inside of the "images" folder.   Place image files like: 1-1.png.
  • If any paragraph text will reference "Table 1-1", it will include 1-1.png in the video file. 

Uploading Audiobook to YouTube

  • First select Image file to be used for mp4 file generation.
  • 3. Make MP4 Files will generate mp4 file for each chapter. This operation uses mp3 files from -Chapters folder. This operation can take about 8 hours. These files can then be uploaded to YouTube. The files will be placed in the folder with the same name as the text file name plus -Videos
  • Make Video File will generate one mp4 file. These files can then be uploaded to YouTube. The file will be generated by merging MP4 chapter files if they are available. MP4 chapter files are not available. The single mp3 file will be used, but the operation can take about 8 hours. The MP4 file will be placed in the folder as the text file and have the same name but with mp4 extension.
  • Generate YouTube index from Chapters mp3 file duration. The index can be used in the Video description or the comment section. Note that MP4 files night be generated with a different duration so the Index might need to be adjusted.

Here is an audiobook I created using this app.

Using the code

Code is using ffmpeg.exe to convert mp3 to mp4 and change mp3 bitrate. Here is the VB.NET code for the main form:

VB.NET
Imports System.Net
Imports System.IO
Public Class Form1

    Dim oAppSetting As New AppSetting()
    Dim bStop As Boolean = False
    Dim oImageList As New Hashtable
    Dim oLineImages As New Hashtable()
    Dim sOpenAiApiKey As String = ""
    Dim sAnthropicKey As String = ""
    Dim sAiProcessFileName As String = "Validate.txt"
    Dim sAIService As String = "gpt-4o-mini" 'gpt-4o,gpt-4o-mini,claude-3-5-sonnet-20240620
    Dim sAIInstructions As String = ""

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        selSilence.SelectedIndex = 0

        oAppSetting.LoadData()
        txtImageFile.Text = oAppSetting.GetValue("ImageFile")
        txtSrcFile.Text = oAppSetting.GetValue("SrcFile")
        txtText.Text = oAppSetting.GetValue("Text")
        txtApiKey.Text = oAppSetting.GetValue("ApiKey")

        sOpenAiApiKey = oAppSetting.GetValue("OpenAiApiKey", sOpenAiApiKey)
        sAnthropicKey = oAppSetting.GetValue("AnthropicKey", sAnthropicKey)
        sAIService = oAppSetting.GetValue("AIService", sAIService)
        sAIInstructions = oAppSetting.GetValue("AIInstructions", sAIInstructions)
        txtFirstChapterName.Text = oAppSetting.GetValue("FirstChapterName", txtFirstChapterName.Text)

        sLanguageCode = oAppSetting.GetValue("LanguageCode", sLanguageCode)
        Dim sLanguageCodes As String = oAppSetting.GetValue("LanguageCodes", "")
        SetComboBoxList(cbLanguage, sLanguageCodes)
        SetComboBox(cbLanguage, sLanguageCode)

        sVoiceId = oAppSetting.GetValue("Voice", sVoiceId)
        Dim sVoiceList As String = oAppSetting.GetValue("Voices", "")
        SetComboBoxList(selVoice, sVoiceList)
        SetComboBox(selVoice, sVoiceId)

        If oAppSetting.GetValue("TextColor") <> "" Then
            btnTextColor.BackColor = Color.FromArgb(oAppSetting.GetValue("TextColor"))
        End If

        If oAppSetting.GetValue("BackColor") <> "" Then
            btnBgColor.BackColor = Color.FromArgb(oAppSetting.GetValue("BackColor"))
        End If

        If oAppSetting.GetValue("BottomMargin") <> "" Then
            txtBottomMargin.Text = oAppSetting.GetValue("BottomMargin")
        End If

        txtLeftMargin.Text = oAppSetting.GetValue("LeftMargin")
        txtFontSize.Text = oAppSetting.GetValue("FontSize", txtFontSize.Text)

        If txtApiKey.Text <> "" Then
            txtApiKey.PasswordChar = "*"
        End If

        If IO.File.Exists(txtSrcFile.Text) = False Then
            txtSrcFile.Text = ""
        End If

        If IO.File.Exists(txtImageFile.Text) = False Then
            txtImageFile.Text = ""
        End If


        UpdateFileGrid()

        Dim sSelectedRowIndex As String = oAppSetting.GetValue("SelectedRowIndex")
        If sSelectedRowIndex <> "" Then
            Dim iRowIndex As Integer = sSelectedRowIndex
            If iRowIndex <> -1 AndAlso iRowIndex < DataGridView1.RowCount Then
                DataGridView1.MultiSelect = False
                DataGridView1.Rows(iRowIndex).Cells(0).Selected = True
                SetupLineText()
            End If
        End If

        Dim sTootip As String = ""

        ToolTip1.AutoPopDelay = 32767
        ToolTip1.SetToolTip(btnProcessTextFile, "Generate MP3 file for each line in the text file. " & vbCrLf &
                            "The file will be placed in the folder with the same name as the text file name. " & vbCrLf &
                            "Each file will be named after the line number in the text file like 0001.mp3. " & vbCrLf &
                            "This means that you should not add or deleted lines to the text file after MP3 files are generated.")

        ToolTip1.SetToolTip(btnChapters, "Generate MP3 file for each chapter. Two blank lines in the text file means new Chapter." & vbCrLf &
                            "The files will be placed in the folder with the same name as the text file name plus -Chapters")

        ToolTip1.SetToolTip(btnMakeVideos, "Generate MP4 file for each chapter. This operation uses mp3 files from -Chapters folder." & vbCrLf &
                            "This operation can take about 8 hours. These files can then be uploaded to YouTube." & vbCrLf &
                            "The files will be placed in the folder with the same name as the text file name plus -Videos")

        ToolTip1.SetToolTip(btnMerge, "Merge MP3 Chapter files into one MP3 file." & vbCrLf &
                            "The MP3 file will be placed in the folder as the text file and have the same name but with MP3 extension.")

        ToolTip1.SetToolTip(btnMakeVideo, "Generate one MP4 file. These file can then be uploaded to YouTube. " & vbCrLf &
                            "The file will be generated by merging MP4 chapter files if they are available. " & vbCrLf &
                            "If MP4 chapter files are not available the single MP3 file will be used but the operation can take about 8 hours." & vbCrLf &
                            "The MP4 file will be placed in the folder as the text file and have the same name but with MP4 extension.")

        ToolTip1.SetToolTip(btnYouTubeIndex, "Generate YouTube index from Chapters MP3 file duration. " & vbCrLf &
                            "The index can be used in the Video description or the comment section. " & vbCrLf &
                            "Note that MP4 files night be generated with a different duration so the Index might need to be adjusted.")

        ToolTip1.SetToolTip(btnPlay, "Select a line in the grid and click play to MP3 file.")
        ToolTip1.SetToolTip(btnStopPlay, "Click to stop the MP3 file playing")
        ToolTip1.SetToolTip(btnDelete, "Select a line in the grid and click delete to MP3 file.")

        ToolTip1.SetToolTip(btnReGenerate, "Select a line in the grid and click Generate (to save the text file if needed) and re-create the MP3 file.")
        ToolTip1.SetToolTip(btnSave, "Save the changes in the text file")
        ToolTip1.SetToolTip(chkPlayOnKeyUp, "This option allows you to listen to the entire book by pressing the arrow down key after selecting a line text.")
        ToolTip1.SetToolTip(selHighlight, "This option helps you with text file correction and editing before to generating the mp3 files.")

        sTootip = "For testing generate mp3 file based on any text"
        ToolTip1.SetToolTip(txtText, sTootip)
        ToolTip1.SetToolTip(btnSayIt, sTootip)

        sTootip = "API Key from Google"
        ToolTip1.SetToolTip(txtApiKey, sTootip)
        ToolTip1.SetToolTip(btnApiKeyShow, sTootip)

        sTootip = "If you have pdf file, the text file can be generated by opening the pdf file in Word and copy and pasting to a text file." & vbCrLf &
                            " Open the text file in a text editor that has line numbers (such as Notepad2).  "
        ToolTip1.SetToolTip(txtSrcFile, sTootip)
        ToolTip1.SetToolTip(btnSrcFile, sTootip)

        sTootip = "Image file to be used for mp4 file generation."
        ToolTip1.SetToolTip(txtImageFile, sTootip)
        ToolTip1.SetToolTip(btnImageFile, sTootip)

        sTootip = "One blank line means 1 second pause.   Use this in case you need to customize the pause duration."
        ToolTip1.SetToolTip(selSilence, sTootip)
        ToolTip1.SetToolTip(btnSilence, sTootip)

        ToolTip1.SetToolTip(chkBackupFile, "Use this option if you want to undo the changes you made to the text file. " &
            "The backup text files will be placed in the folder with the same name as the text file name plus _backup")

        ToolTip1.SetToolTip(btnCreateLineVideos, "Create MP4 for each line.  " &
           "Usefull if you have a image folder and each line references images in this folder.")

        ToolTip1.SetToolTip(btnCheckImages, "Make sure that images get referenced in the text.")

        ToolTip1.SetToolTip(btnProcessChapter, "Process lines from the selected line until a blank line?" &
                            " This is useful for using different voice for different sections.")

        ToolTip1.SetToolTip(urlApiKey, "Profile > API Key")

        ToolTip1.SetToolTip(chkImageText, "Add Chapter file name to the video file image")

        ToolTip1.SetToolTip(btnRenameDown, "Rename file for the selected row and subsequent files by + 1? " & vbCrLf &
                            " This is useful for inserting a line into the text if mp3 files are already generated.")

        ToolTip1.SetToolTip(txtFirstChapterName, "First Chapter Name. Useful for chapter file name " &
                            " because first chapter is usually preceded by  Book title an Author Name.")

        ToolTip1.SetToolTip(btnAiSettings, "OCRed text can have typos.  This module uses AI (Anthropic or OpenAI) to correct them.")
        ToolTip1.SetToolTip(btnValidate, "Create a text file by running a prompt and each line of text against an AI bot.  " &
                            "The output text file will line number tab and AI bot output. Like '001  OK'")
        ToolTip1.SetToolTip(btnReviewAI, "Review changes proposed by the AI bot.  Select and apply changes.")
        ToolTip1.SetToolTip(btnDeleteValid, "Delete mp3 files for the lines the AI bot think need to be changed.")
        ToolTip1.SetToolTip(btnUpdateFileGrid, "Update File Grid")

    End Sub

    Private Sub Form1_FormClosing(sender As Object, e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing

        oAppSetting.SetValue("Voice", cbLanguage.Text)

        sLanguageCode = GetComboBoxVal(cbLanguage, sLanguageCode)
        oAppSetting.SetValue("LanguageCode", sLanguageCode)

        sVoiceId = GetComboBoxVal(selVoice, sVoiceId)
        oAppSetting.SetValue("Voice", sVoiceId)

        oAppSetting.SetValue("SrcFile", txtSrcFile.Text)
        oAppSetting.SetValue("ImageFile", txtImageFile.Text)
        oAppSetting.SetValue("Text", txtText.Text)
        oAppSetting.SetValue("ApiKey", txtApiKey.Text)
        oAppSetting.SetValue("SelectedRowIndex", GetSelectedRowIndex())
        oAppSetting.SetValue("TextColor", btnTextColor.BackColor.ToArgb())
        oAppSetting.SetValue("BackColor", btnBgColor.BackColor.ToArgb())
        oAppSetting.SetValue("BottomMargin", txtBottomMargin.Text)
        oAppSetting.SetValue("LeftMargin", txtLeftMargin.Text)
        oAppSetting.SetValue("FontSize", txtFontSize.Text)
        oAppSetting.SetValue("OpenAiApiKey", sOpenAiApiKey)
        oAppSetting.SetValue("AnthropicKey", sAnthropicKey)
        oAppSetting.SetValue("AIService", sAIService)
        oAppSetting.SetValue("AIInstructions", sAIInstructions)
        oAppSetting.SetValue("FirstChapterName", txtFirstChapterName.Text)
        oAppSetting.SetValue("LanguageCodes", GetComboBoxList(cbLanguage))
        oAppSetting.SetValue("Voices", GetComboBoxList(selVoice))

        oAppSetting.SaveData()
    End Sub

    Private Sub btnStop_Click(sender As Object, e As EventArgs) Handles btnStop.Click
        bStop = True
    End Sub

    Private Sub btnProcessTextFile_Click(sender As Object, e As EventArgs) Handles btnProcessTextFile.Click

        If DataGridView1.RowCount = 0 Then
            Exit Sub
        End If

        If MsgBox("Are you sure you want to process the text file?", vbYesNo) <> vbYes Then
            Exit Sub
        End If

        For iRow = 0 To DataGridView1.RowCount - 1
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
            If oRow.IsNewRow = False Then
                Dim sText As String = DataGridView1.Rows(iRow).Cells("Text").Value & ""
                If Len(sText) > 2500 Then
                    MsgBox("Row " & (iRow + 1) & " is exceeds the max charachter size of 2500 (" & Len(sText) & "), Text:" & sText)
                    Exit Sub
                End If
            End If
        Next

        btnProcessTextFile.Enabled = False
        My.Application.DoEvents()

        ProcessTextFile(0)
        UpdateFileGrid()

        btnProcessTextFile.Enabled = True
        MsgBox("Done")
    End Sub

    Function CheckForSilence(sLine As String) As String
        For i As Integer = 1 To 9
            If sLine.IndexOf("{{" & i.ToString() & "00ms.mp3}}") <> -1 Then
                Return i.ToString() & "00ms.mp3"
            End If
        Next
        Return ""
    End Function

    Sub ProcessTextFile(ByVal iProcessRow As Integer)

        If txtApiKey.Text = "" Then
            MsgBox("API Key is missing")
            Exit Sub
        End If

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
            txtSrcFile.Text = ""
            MsgBox("Text file is blank")
            Exit Sub
        End If

        Dim sMp3FolderePath As String = GetFolderPath("mp3")
        If sMp3FolderePath = "" Then
            MsgBox("Could not find mp3 folder")
            Exit Sub
        End If

        Dim sBlankFilePath As String = sMp3FolderePath & "\1sec.mp3"
        If IO.File.Exists(sBlankFilePath) = False Then
            MsgBox("Could not find " & sBlankFilePath)
            Exit Sub
        End If

        If iProcessRow = 0 Then
            lbCount.Visible = True
            btnStop.Visible = True
            ProgressBar1.Visible = True
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sDestFolderPath As String = Path.Combine(sFolderPath, sFileName)

        If Not System.IO.Directory.Exists(sDestFolderPath) Then
            System.IO.Directory.CreateDirectory(sDestFolderPath)
        End If

        Dim iRows As Integer = GetFileRowsCount(sFilePath)
        If iRows = 0 Then
            Exit Sub
        End If

        Dim iMaxSize As Integer = iRows.ToString().Length

        If iProcessRow = 0 Then
            ProgressBar1.Maximum = iRows
        End If

        Dim iRow As Integer = 0
        Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
        Dim sLine As String = oStreamReader.ReadLine()
        Do Until sLine Is Nothing
            iRow += 1

            If iProcessRow = 0 OrElse iRow = iProcessRow Then

                If iProcessRow = 0 Then
                    lbCount.Text = iRow & "/" & iRows
                End If

                Dim sDestFileBase As String = Microsoft.VisualBasic.Right("000000" & iRow, iMaxSize)
                Dim sDestFileName As String = sDestFileBase & ".mp3"
                Dim sDestFilePath As String = Path.Combine(sDestFolderPath, sDestFileName)

                sLine = Trim(sLine) & ""

                If IO.File.Exists(sDestFilePath) = False Then
                    Dim sSilenceFile As String = CheckForSilence(sLine)
                    If sSilenceFile <> "" Then
                        IO.File.Copy(sMp3FolderePath & "\" & sSilenceFile, sDestFilePath)
                    ElseIf Trim(sLine) = "" Or Trim(sLine) = "{{1sec.mp3}}" Then
                        'Copy Blank mp3 file
                        IO.File.Copy(sBlankFilePath, sDestFilePath)
                    Else
                        If TextToSpeach(sLine, sDestFilePath) Then
                            'Success
                        Else
                            If iProcessRow = 0 Then
                                If MsgBox("Could not generate file for line: " & iRow & ", Stop Processing?", vbYesNo) = vbYes Then
                                    ResetProgressBar()
                                    oStreamReader.Close()
                                    Exit Sub
                                End If
                            Else
                                MsgBox("Could not generate file for line: " & iRow & ", Text: " & sLine)
                            End If

                        End If
                    End If
                End If

            End If 'iProcessRow = 0 OrElse iRow = iProcessRow

            sLine = oStreamReader.ReadLine()

            If iProcessRow = 0 Then
                ProgressBar1.Value = iRow

                My.Application.DoEvents()
                If bStop Then
                    bStop = False
                    MsgBox("Stopped Processing at row " & iRow & ". There are " & iRows & " rows.")
                    Exit Do
                End If
            End If

        Loop

        oStreamReader.Close()

        If iProcessRow = 0 Then
            ResetProgressBar()
        End If

    End Sub

    Private Sub ResetProgressBar()
        lbCount.Visible = False
        btnStop.Visible = False
        ProgressBar1.Value = 1
        ProgressBar1.Visible = False
    End Sub

    Private Function GetFileRowsCount(ByVal sFilePath As String) As Integer
        Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
        Dim sLine As String = oStreamReader.ReadLine()
        Dim iRow As Integer = 0

        Do Until sLine Is Nothing
            iRow += 1
            sLine = oStreamReader.ReadLine()
        Loop

        oStreamReader.Close()

        Return iRow
    End Function

    Private Function GetAssFolderPath() As String
        Dim sAssPath As String = System.Reflection.Assembly.GetExecutingAssembly().Location
        Return System.IO.Path.GetDirectoryName(sAssPath)
    End Function

    Private Function GetFfmpegFile() As String
        'https://www.gyan.dev/ffmpeg/builds/
        Dim sFolderPath As String = GetAssFolderPath()
        Dim sExePath As String = IO.Path.Combine(sFolderPath, "ffmpeg.exe")
        If IO.File.Exists(sExePath) Then
            Return sExePath
        End If

        Dim sFfmpegFolder As String = GetFolderPath("ffmpeg")
        Return sFfmpegFolder & "\bin\ffmpeg.exe"
    End Function

    Private Function GetFolderPath(ByVal sFolderName As String) As String

        Dim sPath As String = GetAssFolderPath()

        For i As Integer = 0 To 3
            Dim sRetPath As String = IO.Path.Combine(sPath, sFolderName)

            If IO.Directory.Exists(sRetPath) Then
                Return sRetPath
            End If

            Try
                sPath = IO.Directory.GetParent(sPath).FullName
            Catch ex As Exception
                Return ""
                'MsgBox("GetFolderPath(), Could not get  parent of: " & sPath)
            End Try

        Next

        Return ""
    End Function

    Private Function GetTempFolder() As String
        Dim sTempFolder As String = GetFolderPath("Temp")
        If IO.Directory.Exists(sTempFolder) = False Then
            sTempFolder = IO.Path.Combine(GetAssFolderPath(), "Temp")

            If IO.Directory.Exists(sTempFolder) = False Then
                IO.Directory.CreateDirectory(sTempFolder)
            End If
        End If

        Return sTempFolder
    End Function

    Private Sub btnSayIt_Click(sender As Object, e As EventArgs) Handles btnSayIt.Click

        If txtApiKey.Text = "" Then
            MsgBox("API Key is missing")
            Exit Sub
        End If

        Dim sTempFolder As String = GetTempFolder()
        Dim sFilePath As String = IO.Path.Combine(sTempFolder, GetGuidFileName("mp3"))

        If TextToSpeach(txtText.Text, sFilePath) Then
            If IO.File.Exists(sFilePath) Then

                Dim sDestFilePath As String = IO.Path.Combine(sTempFolder, PadFileName(txtText.Text) & ".mp3")

                Try
                    If IO.File.Exists(sDestFilePath) Then
                        PlaySoundStop()

                        IO.File.Delete(sDestFilePath)
                    End If

                    IO.File.Move(sFilePath, sDestFilePath)
                    sFilePath = sDestFilePath
                Catch ex As Exception
                    'Ignore
                End Try

                txtTestFile.Visible = True
                txtTestFile.Text = sFilePath

                PlaySound(sFilePath)
            End If
        End If

    End Sub

    Private Function TextToSpeach(sText As String, sFilePath As String) As Boolean

        sLanguageCode = GetComboBoxVal(cbLanguage, sLanguageCode)
        sVoiceId = GetComboBoxVal(selVoice, sVoiceId)
        If sLanguageCode = "" Or sVoiceId = "" Then
            Return False
        End If

        For i As Integer = 1 To 100
            Dim sError As String = TextToSpeach2(sText, sFilePath)
            If sError = "" Then
                Return True

            ElseIf sError.IndexOf("(429) Too Many Requests") <> -1 Then
                'https://cloud.google.com/text-to-speech/docs/basics
                'Rate limits for Tier 3
                'tts-1    100 RPM (requests per minute)

                Threading.Thread.Sleep(1000 * i)

            Else
                Return False
            End If
        Next

        Return False
    End Function



    Private Sub btnLoadLanguages_Click(sender As Object, e As EventArgs) Handles btnLoadLanguages.Click
        LoadVoices("")
    End Sub

    Private Sub btnLoadVoices_Click(sender As Object, e As EventArgs) Handles btnLoadVoices.Click

        If cbLanguage.SelectedIndex <> -1 And cbLanguage.Items.Count > 0 Then
            sLanguageCode = cbLanguage.Items(cbLanguage.SelectedIndex)
        End If

        LoadVoices(sLanguageCode)
    End Sub


    'https://cloud.google.com/text-to-speech/docs/voices
    Dim sLanguageCode As String = "ru-RU"
    Dim sVoiceId As String = "ru-RU-Standard-B" '

    Sub LoadVoices(ByVal sLangCode As String)

        Dim apiEndpoint As String = "https://texttospeech.googleapis.com/v1/voices?languageCode=" & sLangCode
        Dim request As HttpWebRequest = WebRequest.Create(apiEndpoint)
        request.Method = "GET"
        request.ContentType = "application/json"
        request.Headers.Add("X-Goog-Api-Key", txtApiKey.Text)

        Dim response As HttpWebResponse = request.GetResponse()
        If response.StatusCode <> 200 Then
            Exit Sub
        End If

        Dim oLanguageList As New Hashtable
        Dim sSelectedVoice As String = GetComboBoxVal(selVoice, sVoiceId)
        Dim sSelectedLanguage As String = GetComboBoxVal(cbLanguage, sLanguageCode)

        If sLangCode <> "" Then
            selVoice.Items.Clear()
        End If

        Dim streamReader As New StreamReader(response.GetResponseStream())
        Dim sJson As String = streamReader.ReadToEnd()
        Dim oJavaScriptSerializer As New System.Web.Script.Serialization.JavaScriptSerializer
        Dim oJson As Hashtable = oJavaScriptSerializer.Deserialize(Of Hashtable)(sJson)
        Dim oVoices As Object = oJson("voices")
        For i As Integer = 0 To oVoices.Length - 1

            If sLangCode = "" Then
                Dim sLanguageCode2 As String = oVoices(i)("languageCodes")(0)
                oLanguageList(sLanguageCode2) = ""
            Else
                Dim sName As String = oVoices(i)("name")
                'Dim sGender As String = oVoices(i)("ssmlGender")

                selVoice.Items.Add(sName)
            End If
        Next

        If sLangCode = "" Then
            cbLanguage.Items.Clear()

            Dim oSortedList As New SortedList
            For Each oKey As DictionaryEntry In oLanguageList
                oSortedList.Add(oKey.Key, oKey.Key)
            Next

            For Each oKey As DictionaryEntry In oSortedList
                cbLanguage.Items.Add(oKey.Key)
            Next
        End If

        SetComboBox(cbLanguage, sSelectedLanguage)
        SetComboBox(selVoice, sSelectedVoice)

    End Sub

    Private Function GetComboBoxVal(ByRef oComboBox As ComboBox, sDefaultValue As String) As String
        If oComboBox.SelectedIndex = -1 Then
            Return sDefaultValue
        End If

        Return oComboBox.Items(oComboBox.SelectedIndex)
    End Function

    Private Sub SetComboBox(ByRef oComboBox As ComboBox, sValue As String)
        For i As Integer = 0 To oComboBox.Items.Count - 1
            If oComboBox.Items(i) = sValue Then
                oComboBox.SelectedIndex = i
                Exit Sub
            End If
        Next
    End Sub

    Private Function GetComboBoxList(ByRef oComboBox As ComboBox) As String
        Dim sRet As String = ""
        For i As Integer = 0 To oComboBox.Items.Count - 1
            If sRet <> "" Then sRet += ","
            sRet += oComboBox.Items(i)
        Next
        Return sRet
    End Function

    Private Sub SetComboBoxList(ByRef oComboBox As ComboBox, sValue As String)

        If sValue = "" Then
            Exit Sub
        End If

        oComboBox.Items.Clear()
        Dim oList As String() = sValue.Split(",")
        For Each sItem In oList
            oComboBox.Items.Add(sItem)
        Next
    End Sub

    Private Function TextToSpeach2(sText As String, sFilePath As String) As String

        If Trim(sText) = "" Then
            Return "No text provided"
        End If

        If txtApiKey.Text = "" Then
            Return "Set API Key"
        End If

        System.Net.ServicePointManager.SecurityProtocol =
            System.Net.SecurityProtocolType.Ssl3 Or
            System.Net.SecurityProtocolType.Tls12 Or
            System.Net.SecurityProtocolType.Tls11 Or
            System.Net.SecurityProtocolType.Tls

        'https://cloud.google.com/text-to-speech?hl=en
        'https://stackoverflow.com/questions/50705976/google-cloud-text-to-speech-api-quickstart-example
        'https://github.com/GoogleCloudPlatform/cloud-shell-tutorials/blob/master/ml/cloud-tts-intro/tutorial.md
        Dim apiEndpoint As String = "https://texttospeech.googleapis.com/v1beta1/text:synthesize"
        Dim request As HttpWebRequest = WebRequest.Create(apiEndpoint)
        request.Method = "POST"
        request.ContentType = "application/json"
        request.Accept = "audio/mpeg"
        request.Headers.Add("X-Goog-Api-Key", txtApiKey.Text)


        Dim data As String = "{"
        data += " ""audioConfig"":{"
        data += " ""audioEncoding"":""MP3"","
        data += " ""effectsProfileId"": [""small-bluetooth-speaker-class-device""],"
        data += " ""pitch"":0,"
        data += " ""speakingRate"":1"
        data += "},"
        data += " ""input"": {""text"": """ & PadQuotes(sText) & """},"
        data += " ""voice"": {""languageCode"": """ & sLanguageCode & """, ""name"":""" & sVoiceId & """}"
        data += "}"

        Using streamWriter As New StreamWriter(request.GetRequestStream())
            streamWriter.Write(data)
            streamWriter.Flush()
            streamWriter.Close()
        End Using

        Dim response As HttpWebResponse = Nothing

        Try
            response = request.GetResponse()
        Catch ex As Exception

            If ex.Message.IndexOf("The remote server returned an error: (400) Bad Request.") <> -1 Then
                If MsgBox("(400) Bad Request. Open request in Notepad?", vbYesNo) = vbYes Then
                    OpenNotepad(data)
                End If
            End If

            Return ex.Message
        End Try

        If response.StatusCode = 200 Then
            Try

                Dim streamReader As New StreamReader(response.GetResponseStream())
                Dim sJson As String = streamReader.ReadToEnd()
                Dim oJavaScriptSerializer As New System.Web.Script.Serialization.JavaScriptSerializer
                Dim oJson As Hashtable = oJavaScriptSerializer.Deserialize(Of Hashtable)(sJson)
                Dim sAudioContent As String = oJson("audioContent")
                Dim mp3Bytes As Byte() = Convert.FromBase64String(sAudioContent)
                File.WriteAllBytes(sFilePath, mp3Bytes)

                Return ""
            Catch ex As Exception
                Return "Error: TextToSpeach2 - audioContent: " & ex.Message
            End Try

        Else
            Return "StatusCode: " & response.StatusCode
        End If

    End Function

    Sub OpenNotepad(ByVal s As String)
        ' Generate a GUID
        Dim sGuid As String = Guid.NewGuid().ToString("N")

        ' Get the path to the Temp folder
        Dim tempFolderPath As String = Path.GetTempPath()
        Dim sFilePath As String = Path.Combine(tempFolderPath, sGuid & ".txt")

        ' Write the content to the file
        File.WriteAllText(sFilePath, s)

        ' Start Notepad with the created file
        Process.Start("notepad.exe", sFilePath)
    End Sub

    Private Sub SetVoiceSelect(sVoice As String)
        For i As Integer = 0 To cbLanguage.Items.Count - 1
            If cbLanguage.Items(i) = sVoice Then
                cbLanguage.SelectedIndex = i
                Exit For
            End If
        Next
    End Sub

    Private Function PadQuotes(ByVal s As String) As String

        If s.IndexOf("\") <> -1 Then
            s = Replace(s, "\", "\\")
        End If

        If s.IndexOf(vbCrLf) <> -1 Then
            s = Replace(s, vbCrLf, "\n")
        End If

        If s.IndexOf(vbCr) <> -1 Then
            s = Replace(s, vbCr, "\r")
        End If

        If s.IndexOf(vbLf) <> -1 Then
            s = Replace(s, vbLf, "\f")
        End If

        If s.IndexOf(vbTab) <> -1 Then
            s = Replace(s, vbTab, "\t")
        End If

        If s.IndexOf("""") = -1 Then
            Return s
        Else
            Return Replace(s, """", "\""")
        End If
    End Function

    Dim oPlayer As Object = Nothing

    Sub PlaySound()
        Dim sFilePath As String = GetSelectedFielPath()
        If sFilePath <> "" Then
            PlaySound(sFilePath)
        Else
            MsgBox("MP3 file does not exist " & sFilePath)
        End If
    End Sub

    Sub PlaySound(sSoundFile As String)

        If IO.File.Exists(sSoundFile) = False Then
            Exit Sub
        End If

        PlaySoundStop()
        btnStopPlay.Enabled = True

        oPlayer = CreateObject("WMPlayer.OCX")
        oPlayer.URL = sSoundFile
        oPlayer.controls.play()
    End Sub

    Sub PlaySoundStop()
        If oPlayer IsNot Nothing Then
            oPlayer.controls.stop()
            oPlayer.Close()
            oPlayer = Nothing
            btnStopPlay.Enabled = False
        End If
    End Sub

    Private Sub btnStopPlay_Click(sender As Object, e As EventArgs) Handles btnStopPlay.Click
        PlaySoundStop()
    End Sub

    Public Function GetGuidFileName(ByVal sExt As String) As String
        Return System.Guid.NewGuid().ToString("N") + "." + sExt
    End Function

    Private Sub btnSrcFile_Click(sender As Object, e As EventArgs) Handles btnSrcFile.Click
        OpenFileDialog1.FileName = txtSrcFile.Text
        OpenFileDialog1.Title = "Open Text File"
        OpenFileDialog1.Filter = "TXT files|*.txt"
        OpenFileDialog1.ShowDialog()

        If OpenFileDialog1.FileName <> "" Then
            txtSrcFile.Text = OpenFileDialog1.FileName
        End If

        UpdateFileGrid()
    End Sub

    Private Sub btnImageFile_Click(sender As Object, e As EventArgs) Handles btnImageFile.Click

        OpenFileDialog1.FileName = txtSrcFile.Text
        OpenFileDialog1.Title = "Image File"
        OpenFileDialog1.Filter = "Image files|*.jpg;*.png;*.webp"
        OpenFileDialog1.ShowDialog()

        If OpenFileDialog1.FileName <> "" Then
            txtImageFile.Text = OpenFileDialog1.FileName
        End If

    End Sub



    Private Sub UpdateFileGrid(Optional ByVal iProcessRow As Integer = 0)
        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" OrElse File.Exists(sFilePath) = False Then
            txtSrcFile.Text = ""
            DataGridView1.DataSource = Nothing
            DataGridView1.Update()
            Exit Sub
        End If

        Dim sFolderImageSeparator As String = ". "

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sDestFolderPath As String = Path.Combine(sFolderPath, sFileName)
        Dim iRowIndex As Integer = GetSelectedRowIndex()

        If iProcessRow = 0 Then
            'Reload all rows

            oImageList = New Hashtable

            Dim sImageFolderPath As String = Path.Combine(sFolderPath, "images")
            If System.IO.Directory.Exists(sImageFolderPath) Then
                Dim oFolders As String() = System.IO.Directory.GetDirectories(sImageFolderPath)
                For Each sSubFolder As String In oFolders
                    Dim sSubFolderName As String = (New System.IO.DirectoryInfo(sSubFolder)).Name
                    Dim oFiles As String() = System.IO.Directory.GetFiles(sSubFolder)
                    For Each sImgFilePath As String In oFiles
                        Dim sImgFileName As String = System.IO.Path.GetFileNameWithoutExtension(sImgFilePath)
                        oImageList(sImgFilePath) = sSubFolderName & sFolderImageSeparator & sImgFileName
                    Next
                Next
            End If

            If oImageList.Count > 0 Then
                gbFigures.Visible = True
            End If
        End If

        Dim oTable As Data.DataTable = GetDataTableFromFolder(sFilePath, sDestFolderPath)
        DataGridView1.DataSource = oTable
        DataGridView1.Update()

        DataGridView1.Columns("Size").Visible = False
        DataGridView1.Columns("FilePath").Visible = False
        'DataGridView1.Columns("Length").DefaultCellStyle.Format = "#,#"

        DataGridColor()
        DataGridResize()

        If iRowIndex <> -1 And iRowIndex < DataGridView1.RowCount Then
            DataGridView1.MultiSelect = False
            DataGridView1.Rows(iRowIndex).Cells(0).Selected = True
        End If


    End Sub


    Private Function GetNoImageChapters() As Hashtable
        Dim oRet As New Hashtable

        If oImageList.Count = 0 Then
            Return oRet
        End If

        Dim sChapterName As String = txtFirstChapterName.Text
        Dim oChapters As New Hashtable
        Dim iChapterCount As Integer = 1

        For iRow = 0 To DataGridView1.RowCount - 1
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
            If oRow.IsNewRow = False Then
                If oRow.Cells("Text").Style.BackColor = Color.GreenYellow Then
                    sChapterName = DataGridView1.Rows(iRow + 1).Cells("Text").Value & ""

                    Dim sSilenceFile As String = CheckForSilence(sChapterName)
                    If sSilenceFile <> "" Then
                        sChapterName = Replace(sChapterName, "{{" & sSilenceFile & "}}", "")
                    End If

                    sChapterName = PadFileName(Trim(sChapterName))
                    iChapterCount += 1
                End If

                Dim sChapterName2 As String = Microsoft.VisualBasic.Right("000" & iChapterCount, 3) & " " & Microsoft.VisualBasic.Left(sChapterName, 100)

                If oChapters.ContainsKey(sChapterName2) = False Then
                    oChapters(sChapterName2) = False
                End If

                Dim sImages As String = oRow.Cells("Images").Value & ""
                If sImages <> "" Then
                    oChapters(sChapterName2) = True
                End If

            End If
        Next

        For Each oEntry As DictionaryEntry In oChapters
            If oEntry.Value = False Then
                oRet(oEntry.Key) = ""
            End If
        Next

        Return oRet
    End Function

    Private Sub btnCheckImages_Click(sender As Object, e As EventArgs) Handles btnCheckImages.Click
        CheckImages()
    End Sub

    Private Sub CheckImages()
        'Make sure that images get referenced in the text

        If oImageList.Count = 0 Then
            MsgBox("No images folder with subfolders is found")
            Exit Sub
        End If

        txtLine.Text = ""

        DataGridView1.MultiSelect = False
        DataGridView1.Rows(0).Cells(0).Selected = True
        DataGridView1.Rows(0).Cells(0).Selected = False

        For Each oImgEntry As DictionaryEntry In oImageList
            Dim sImgName As String = oImgEntry.Value
            Dim sImgName2 As String = PadImageName(sImgName)
            Dim bFound As Boolean = False

            For iRow = 0 To DataGridView1.RowCount - 1
                Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
                If oRow.IsNewRow = False Then
                    Dim sText As String = oRow.Cells("Text").Value & ""
                    If sText.ToLower().IndexOf(sImgName.ToLower()) <> -1 Then
                        bFound = True
                    ElseIf sText.ToLower().IndexOf(sImgName2.ToLower()) <> -1 Then
                        bFound = True
                    End If
                End If
            Next

            If bFound = False Then
                txtLine.AppendText(sImgName & vbCrLf)
            End If
        Next

    End Sub

    Private Function GetLineImages(ByVal sLine As String) As String
        Dim sRet As String = ""

        For Each oImgEntry As DictionaryEntry In oImageList
            Dim sImgName As String = oImgEntry.Value
            Dim sImgName2 As String = PadImageName(sImgName)

            If sLine.ToLower().IndexOf(sImgName.ToLower()) <> -1 Then
                If sRet <> "" Then sRet += ", "
                sRet += sImgName
            ElseIf sLine.ToLower().IndexOf(sImgName2.ToLower()) <> -1 Then
                If sRet <> "" Then sRet += ", "
                sRet += sImgName
            End If
        Next

        Return sRet
    End Function

    Private Function GetLineImagePaths(ByVal sLine As String) As String
        Dim sRet As String = ""

        For Each oImgEntry As DictionaryEntry In oImageList
            Dim sImgPath As String = oImgEntry.Key
            Dim sImgName As String = oImgEntry.Value
            Dim sImgName2 As String = PadImageName(sImgName)

            If sLine.ToLower().IndexOf(sImgName.ToLower()) <> -1 Then
                If sRet <> "" Then sRet += ","
                sRet += sImgPath
            ElseIf sLine.ToLower().IndexOf(sImgName2.ToLower()) <> -1 Then
                If sRet <> "" Then sRet += ","
                sRet += sImgPath
            End If
        Next

        Return sRet
    End Function

    Private Function PadImageName(s As String) As String
        If IsNumeric(Microsoft.VisualBasic.Right(s, 1)) Then
            Return s
        Else
            Return s.Substring(0, s.Length - 1)
        End If
    End Function

    Private Sub DataGridColor()

        Dim sHighlight As String = selHighlight.Text
        Dim iBlankRowCount As Integer = 0

        For iRow = 0 To DataGridView1.RowCount - 1
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
            If oRow.IsNewRow = False Then
                Dim iSize As Integer = oRow.Cells("Size").Value

                If iSize = 5856 Then
                    For Each oCell As DataGridViewCell In oRow.Cells
                        oCell.Style.BackColor = Color.LightBlue
                    Next
                ElseIf iSize = 0 Then
                    For Each oCell As DataGridViewCell In oRow.Cells
                        oCell.Style.BackColor = Color.LightCoral
                    Next
                Else
                    For Each oCell As DataGridViewCell In oRow.Cells
                        oCell.Style.BackColor = Color.White
                    Next
                End If

                Dim sText As String = oRow.Cells("Text").Value & ""

                If Trim(sText) = "" Then
                    iBlankRowCount += 1
                Else
                    iBlankRowCount = 0
                End If

                If iBlankRowCount > 1 Then
                    'New Chapter
                    For Each oCell As DataGridViewCell In oRow.Cells
                        oCell.Style.BackColor = Color.GreenYellow
                    Next

                    If iRow < DataGridView1.Rows.Count Then
                        Dim sName1 As String = DataGridView1.Rows(iRow + 1).Cells("Name").Value & ""
                        Dim sText1 As String = DataGridView1.Rows(iRow + 1).Cells("Text").Value & ""
                        Dim sSilenceFile As String = CheckForSilence(sText1)
                        If sSilenceFile <> "" Then
                            sText1 = Replace(sText1, "{{" & sSilenceFile & "}}", "")
                        End If
                    End If

                End If

                If Len(sText) > 4996 Then
                    'https://community.openai.com/t/text-to-speech-api-limit-speech-generation/558166 
                    oRow.Cells("Size").Style.BackColor = Color.Red
                End If

                If sHighlight <> "" Then

                    Dim sFirstChar As String = Microsoft.VisualBasic.Left(sText, 1)

                    Select Case sHighlight
                        Case "Begins with number"
                            If IsNumeric(sFirstChar) Then
                                oRow.Cells("Text").Style.BackColor = Color.Yellow
                            End If

                        Case "Contains number"
                            If System.Text.RegularExpressions.Regex.IsMatch(sText, "\b\w+\s*\d+\b") Then
                                oRow.Cells("Text").Style.BackColor = Color.Yellow
                            End If

                        Case "Begins with lower case character"
                            If sFirstChar <> UCase(sFirstChar) Then
                                oRow.Cells("Text").Style.BackColor = Color.Yellow
                            End If

                        Case "Contains two or more uppercase characters in the row"
                            If System.Text.RegularExpressions.Regex.IsMatch(sText, "[A-Z][A-Z]") Then
                                oRow.Cells("Text").Style.BackColor = Color.Yellow
                            End If
                    End Select
                End If


            End If
        Next
    End Sub

    Private Sub DataGridResize()
        If DataGridView1.Columns.Count > 3 Then
            Dim w As Integer = DataGridView1.Width - 85
            w = w - DataGridView1.Columns("Name").Width
            w = w - DataGridView1.Columns("Length").Width
            DataGridView1.Columns("Text").Width = Math.Max(w, 200)
        End If
    End Sub

    Private Sub DataGridView1_Resize(sender As Object, e As EventArgs) Handles DataGridView1.Resize
        DataGridResize()
    End Sub

    Private Sub DataGridView1_Sorted(sender As Object, e As EventArgs) Handles DataGridView1.Sorted
        DataGridColor()
    End Sub

    Function GetStreamReader(ByVal sFilePath As String) As IO.StreamReader
        Return New System.IO.StreamReader(sFilePath, System.Text.Encoding.Default)
    End Function

    Private Function GetDataTableFromFolder(ByVal sFilePath As String, ByVal sFolderPath As String) As Data.DataTable

        Dim iSingleMp3FileSize As Integer = 0
        Dim iSingleMp3Seconds As Integer = 0
        Dim sSingleMp3FilePath As String = Path.Combine(Path.GetDirectoryName(sFilePath), Path.GetFileNameWithoutExtension(sFilePath) & ".mp3")
        If IO.File.Exists(sSingleMp3FilePath) Then
            Dim oMP3Info As New Monotic.Multimedia.MP3.MP3Info(sSingleMp3FilePath)
            iSingleMp3Seconds = oMP3Info.Length
            Dim oFileInfo As New IO.FileInfo(sSingleMp3FilePath)
            iSingleMp3FileSize = oFileInfo.Length
        End If
        Dim bUseSingleMp3 As Boolean = sSingleMp3FilePath <> "" AndAlso iSingleMp3FileSize > 0

        Dim iStart As Integer = 0
        Dim oTable As New Data.DataTable
        oTable.Columns.Add(New Data.DataColumn("Name"))
        oTable.Columns.Add(New Data.DataColumn("Length", System.Type.GetType("System.Int64")))
        oTable.Columns.Add(New Data.DataColumn("Start", System.Type.GetType("System.Int64")))
        oTable.Columns.Add(New Data.DataColumn("Start2"))
        oTable.Columns.Add(New Data.DataColumn("Text"))
        oTable.Columns.Add(New Data.DataColumn("FilePath"))
        oTable.Columns.Add(New Data.DataColumn("Size", System.Type.GetType("System.Int64")))
        oTable.Columns.Add(New Data.DataColumn("TextSize", System.Type.GetType("System.Int64")))

        If oImageList.Count > 0 Then
            oTable.Columns.Add(New Data.DataColumn("Images"))
        End If

        Dim iRows As Integer = GetFileRowsCount(sFilePath)
        Dim iMaxSize As Integer = iRows.ToString().Length

        Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
        Dim iRow As Integer = 0
        Dim sLine As String = oStreamReader.ReadLine()
        Do Until sLine Is Nothing
            Dim oDataRow As DataRow = oTable.NewRow()

            If bUseSingleMp3 Then
                oDataRow("Start") = (iStart / iSingleMp3FileSize) * (iSingleMp3Seconds * 1.0)
            Else
                oDataRow("Start") = iStart + (iRow * 0.945) ' 300ms
            End If

            iRow += 1
            Dim sSrcFileBase As String = Microsoft.VisualBasic.Right("000000" & iRow, iMaxSize)
            Dim sSrcFilePath As String = Path.Combine(sFolderPath, sSrcFileBase & ".mp3")
            If IO.File.Exists(sSrcFilePath) Then
                Dim oFileInfo As New IO.FileInfo(sSrcFilePath)
                oDataRow("Size") = oFileInfo.Length
                oDataRow("FilePath") = sSrcFilePath

                If bUseSingleMp3 Then
                    oDataRow("Length") = (oFileInfo.Length / iSingleMp3FileSize) * (iSingleMp3Seconds * 1.0)
                    iStart += oFileInfo.Length
                    iStart += 2657 '(2,657 bytes) '300ms.mp3
                Else
                    Dim iLength As Integer = 0

                    Try
                        Dim oMP3Info As New Monotic.Multimedia.MP3.MP3Info(sSrcFilePath)
                        iLength = oMP3Info.Length
                    Catch ex As Exception
                        'Ignore any error
                    End Try

                    oDataRow("Length") = iLength
                    iStart += iLength
                End If
            Else
                oDataRow("Size") = 0
                oDataRow("Length") = 0
                oDataRow("FilePath") = ""
            End If

            oDataRow("Start2") = TimeSpan.FromSeconds(oDataRow("Start")).ToString()
            oDataRow("Name") = sSrcFileBase
            oDataRow("Text") = sLine
            oDataRow("TextSize") = Len(sLine)

            If oImageList.Count > 0 Then
                Dim sLineImages As String = GetLineImages(sLine)
                If sLineImages <> "" Then
                    oDataRow("Images") = sLineImages
                    oLineImages(sSrcFileBase) = GetLineImagePaths(sLine)
                End If
            End If

            oTable.Rows.Add(oDataRow)
            sLine = oStreamReader.ReadLine()
        Loop

        oStreamReader.Close()
        Return oTable
    End Function

    Private Sub btnPlay_Click(sender As Object, e As EventArgs) Handles btnPlay.Click
        PlaySound()
    End Sub

    Private Sub btnDelete_Click(sender As Object, e As EventArgs) Handles btnDelete.Click
        Dim sFilePath As String = DeleteFile("Delete")
        If sFilePath <> "" Then
            UpdateFileGrid()
        Else
            MsgBox("MP3 file does not exist " & sFilePath)
        End If
    End Sub

    Function DeleteFile(ByVal sAction As String) As String
        Dim sFilePath As String = GetSelectedFielPath()
        If IO.File.Exists(sFilePath) = False Then
            Return ""
        End If

        If MsgBox(sAction & " file " & Path.GetFileName(sFilePath) & "?", MsgBoxStyle.YesNo, sAction & " file") <> vbYes Then
            Return ""
        End If

        PlaySoundStop()

        Try
            IO.File.Delete(sFilePath)
        Catch ex As Exception
            MsgBox("Could Not delete file " & sFilePath & " " & ex.Message)
            Return ""
        End Try

        Return sFilePath
    End Function

    Private Sub btnRegenerate_Click(sender As Object, e As EventArgs) Handles btnReGenerate.Click

        Dim iSelectedRowIndex As Integer = GetSelectedRowIndex()
        If iSelectedRowIndex = -1 Then
            Exit Sub
        End If

        Dim sFilePath As String = DeleteFile("Regenerate")
        If sFilePath = "" Then
            'MsgBox("MP3 file does not exist " & sFilePath)
            'Exit Sub
        End If

        btnReGenerate.Enabled = False
        My.Application.DoEvents()

        If btnSave.Visible = True Then
            SaveTextFile()
        End If

        ProcessTextFile(iSelectedRowIndex + 1)
        UpdateFileGrid(iSelectedRowIndex + 1)
        PlaySound()

        btnReGenerate.Enabled = True

    End Sub

    Sub MergeFolder(sFolderPath, sFilePath)

        If System.IO.Directory.Exists(sFolderPath) = False Then
            MsgBox("Folder does Not exist " & sFolderPath)
            Exit Sub
        End If

        If IO.File.Exists(sFilePath) Then
            IO.File.Delete(sFilePath)
        End If

        Dim oProcess As New System.Diagnostics.Process()
        Dim startInfo As New System.Diagnostics.ProcessStartInfo()
        startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
        startInfo.FileName = "cmd.exe"
        startInfo.Arguments = "/C copy /b """ & sFolderPath & "\*.mp3"" """ & sFilePath & """"
        oProcess.StartInfo = startInfo
        oProcess.Start()
        oProcess.WaitForExit()
    End Sub

    Function GetPauseFilePath() As String
        Dim sMp3FolderePath As String = GetFolderPath("mp3")
        Return sMp3FolderePath & "\300ms.mp3"
    End Function

    Private Sub btnChapters_Click(sender As Object, e As EventArgs) Handles btnChapters.Click

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" Then
            MsgBox("Text file is blank")
            Exit Sub
        End If

        If IO.File.Exists(sFilePath) = False Then
            txtSrcFile.Text = ""
            MsgBox("Text file is blank")
            Exit Sub
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)

        Dim sSrcFolderPath As String = Path.Combine(sFolderPath, sFileName)
        If System.IO.Directory.Exists(sSrcFolderPath) = False Then
            MsgBox("Source folder Is blank: " & sSrcFolderPath)
            Exit Sub
        End If

        Dim sPauseFilePath As String = GetPauseFilePath()
        If IO.File.Exists(sPauseFilePath) = False Then
            MsgBox("Could not find " & sPauseFilePath)
            Exit Sub
        End If

        Dim sDstFolderPath As String = Path.Combine(sFolderPath, sFileName & "-Chapters")
        If System.IO.Directory.Exists(sDstFolderPath) Then

            'ChangeFolderBitRate(sDstFolderPath)
            'SetFileTags(sDstFolderPath, sFileName)

            Try
                EmptyFolder(sDstFolderPath)
            Catch ex As Exception
                MsgBox("Could not empty folder " & sDstFolderPath)
                Exit Sub
            End Try

            System.Threading.Thread.Sleep(1000)
        End If

        If System.IO.Directory.Exists(sDstFolderPath) = False Then
            System.IO.Directory.CreateDirectory(sDstFolderPath)
        End If

        Dim iRows As Integer = GetFileRowsCount(sFilePath)
        If iRows = 0 Then
            Exit Sub
        End If
        Dim iMaxSize As Integer = iRows.ToString().Length

        btnChapters.Enabled = False
        My.Application.DoEvents()

        Dim iRow As Integer = 0
        Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
        Dim iBlankCount As Integer = 0
        Dim iChapterCount As Integer = 1
        Dim sChapterName As String = txtFirstChapterName.Text
        Dim sLine As String = oStreamReader.ReadLine()
        Do Until sLine Is Nothing
            iRow += 1

            If iBlankCount = 2 AndAlso Trim(sLine) <> "" Then
                Dim sSilenceFile As String = CheckForSilence(sLine)
                If sSilenceFile <> "" Then
                    'Chapter name might need to be repeated twice
                    'One time for file name and second time after the section header
                    'This feature lets you ignore first line for TTS and to be used for chapter file name only
                    sLine = Replace(sLine, "{{" & sSilenceFile & "}}", "")
                End If

                sChapterName = PadFileName(Trim(sLine))
                iChapterCount += 1
            End If

            Dim sChapterName2 As String = Microsoft.VisualBasic.Right("000" & iChapterCount, 3) & " " & Microsoft.VisualBasic.Left(sChapterName, 100)
            Dim sChapterFolderPath As String = Path.Combine(sDstFolderPath, sChapterName2)
            If System.IO.Directory.Exists(sChapterFolderPath) = False Then
                Try
                    System.IO.Directory.CreateDirectory(sChapterFolderPath)
                Catch ex As Exception
                    MsgBox("Could not create Chapters folder Line: " & iRow & vbCrLf & sChapterName & vbCrLf & ex.Message)
                    Exit Sub
                End Try
            End If

            Dim sSrcFileBase As String = Microsoft.VisualBasic.Right("000000" & iRow, iMaxSize)
            Dim sSrcFilePath As String = Path.Combine(sSrcFolderPath, sSrcFileBase & ".mp3")

            If IO.File.Exists(sSrcFilePath) Then
                Dim sDestFilePath As String = Path.Combine(sChapterFolderPath, sSrcFileBase & "0.mp3")
                IO.File.Copy(sSrcFilePath, sDestFilePath)

                sDestFilePath = Path.Combine(sChapterFolderPath, sSrcFileBase & "1.mp3")
                If IO.File.Exists(sDestFilePath) = False Then
                    IO.File.Copy(sPauseFilePath, sDestFilePath)
                End If
            End If

            If Trim(sLine) = "" Then
                iBlankCount += 1
            Else
                iBlankCount = 0
            End If

            sLine = oStreamReader.ReadLine()
        Loop

        oStreamReader.Close()

        System.Threading.Thread.Sleep(100)

        For Each sSubFolder As String In IO.Directory.GetDirectories(sDstFolderPath)
            Dim sDestFilePath As String = sSubFolder & ".mp3"
            MergeFolder(sSubFolder, sDestFilePath)
        Next

        For Each sSubFolder As String In IO.Directory.GetDirectories(sDstFolderPath)
            System.IO.Directory.Delete(sSubFolder, True)
        Next

        ChangeFolderBitRate(sDstFolderPath)
        SetFileTags(sDstFolderPath, sFileName)
        btnChapters.Enabled = True
        MsgBox("Done")

    End Sub

    Sub ChangeFolderBitRate(ByVal sDstFolderPath As String)
        Dim oFiles As String() = System.IO.Directory.GetFiles(sDstFolderPath)
        For Each sFile In oFiles
            Dim oFileInfo As New System.IO.FileInfo(sFile)
            If oFileInfo.Extension.ToLower() = ".mp3" Then
                Dim sChangedFilePath As String = ChangeMp3FileBitRate(sFile)
                If IO.File.Exists(sChangedFilePath) Then
                    oFileInfo.Delete()
                    IO.File.Move(sChangedFilePath, sFile)
                End If
            End If
        Next
    End Sub

    Function ChangeMp3FileBitRate(ByVal sFilePath As String) As String

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sOutputFilePath As String = Path.Combine(sFolderPath, sFileName & "_192.mp3")

        If File.Exists(sOutputFilePath) Then
            Return sOutputFilePath
        End If

        Dim sFfmpegFolder As String = GetFolderPath("ffmpeg")
        Dim sFfmpegFile As String = sFfmpegFolder & "\bin\ffmpeg.exe"
        If IO.File.Exists(sFfmpegFile) = False Then
            MsgBox("ffmpeg.exe file is missing: " & sFfmpegFile)
            Return sOutputFilePath
        End If

        Dim oProcess As New System.Diagnostics.Process()
        Dim startInfo As New System.Diagnostics.ProcessStartInfo()
        startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
        startInfo.FileName = sFfmpegFile
        startInfo.Arguments = "-i """ & sFilePath & """ -b:a ""192k"" """ & sOutputFilePath & """"
        oProcess.StartInfo = startInfo
        oProcess.Start()
        oProcess.WaitForExit()

        Return sOutputFilePath
    End Function


    Private Sub SetFileTags(sDstFolderPath As String, sAlbum As String)
        Dim oFiles As String() = System.IO.Directory.GetFiles(sDstFolderPath)
        For Each sFile In oFiles
            Dim oFileInfo As New System.IO.FileInfo(sFile)
            If oFileInfo.Extension.ToLower() = ".mp3" Then
                Dim oMP3Info As New Monotic.Multimedia.MP3.MP3Info(sFile)
                oMP3Info.ID3v1Tag.Album = Microsoft.VisualBasic.Left(sAlbum, 30)
                oMP3Info.ID3v1Tag.Artist = ""
                oMP3Info.ID3v1Tag.Title = TrimAlbum(oFileInfo.Name)
                oMP3Info.Update()
                'Console.WriteLine(oFileInfo.Name)
            End If
        Next
    End Sub

    Function PadImageText(ByVal s As String)
        Dim i As Integer = s.IndexOf(" ")
        If i = -1 Then
            Return s
        End If

        If IsNumeric(s.Substring(0, i)) Then
            Return Trim(s.Substring(i))
        End If

        Return s
    End Function

    Sub CreateTextFile(sSubFolder As String)
        Dim sHtmlFilePath As String = Path.Combine(sSubFolder, "Test.htm")

        If IO.File.Exists(sHtmlFilePath) Then
            IO.File.Delete(sHtmlFilePath)
        End If

        Dim sw As New StreamWriter(sHtmlFilePath, False)
        Dim oFiles As String() = System.IO.Directory.GetFiles(sSubFolder)
        Dim iFileCount As Integer = 0

        sw.WriteLine("<html>")
        sw.WriteLine("<body>")
        sw.WriteLine("<style>")
        sw.WriteLine(".FileName{font-size: 30px}")
        sw.WriteLine("</style>")
        sw.WriteLine("<table border=1>")
        sw.WriteLine("<tr>")
        For Each sPath As String In IO.Directory.GetFiles(sSubFolder)
            If Path.GetExtension(sPath) = ".mp4" Then
                iFileCount += 1
                Dim sFileName As String = Path.GetFileName(sPath)
                Dim oFileInfo As New IO.FileInfo(sPath)
                sw.WriteLine("<td class='FileName'><div>" & sFileName & "</div><div>" & oFileInfo.Length & "</div></td>" &
                            "<td><video src='" & sFileName & "'></video></td>")

                If iFileCount > 1 AndAlso iFileCount Mod 3 = 0 Then
                    sw.WriteLine("</tr><tr>")
                End If

            End If
        Next
        sw.WriteLine("</tr>")
        sw.WriteLine("</table>")

        sw.WriteLine("<script>")
        sw.WriteLine("document.querySelectorAll('video').forEach(function(video) {")
        sw.WriteLine(" video.addEventListener('dblclick', function() {")
        sw.WriteLine("this.controls = !this.controls;")
        sw.WriteLine("});")
        sw.WriteLine("});")
        sw.WriteLine("</script>")

        sw.WriteLine("</body>")
        sw.WriteLine("</html>")
        sw.Close()
    End Sub

    Private Sub btnCreateLineVideos_Click(sender As Object, e As EventArgs) Handles btnCreateLineVideos.Click

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
            txtSrcFile.Text = ""
            MsgBox("Source text file Is blank")
            Exit Sub
        End If

        Dim iRows As Integer = GetFileRowsCount(sFilePath)
        If iRows = 0 Then
            Exit Sub
        End If

        Dim sImageFilePath As String = txtImageFile.Text
        If sImageFilePath = "" Then
            MsgBox("No image is selected!")
            Exit Sub
        End If

        Dim iMaxSize As Integer = iRows.ToString().Length
        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sSrcFolderPath As String = Path.Combine(sFolderPath, sFileName)
        If System.IO.Directory.Exists(sSrcFolderPath) = False Then
            MsgBox("Source folder Is blank: " & sSrcFolderPath)
            Exit Sub
        End If

        Dim sFfmpegFile As String = GetFfmpegFile()
        If IO.File.Exists(sFfmpegFile) = False Then
            MsgBox("ffmpeg.exe file is missing: " & sFfmpegFile)
            Exit Sub
        End If

        If MsgBox("Creating video files might take hours.  " &
                "A small video mp4 file will be created for each mp3 line file.  " &
                "Existing video files will be skipped.  " &
              "  Are you sure you want to do this?", vbYesNo) <> vbYes Then
            Exit Sub
        End If

        If chkImageText.Checked Then
            If btnBgColor.BackColor = btnTextColor.BackColor Then
                MsgBox("Background and text colrs are the same!")
                Exit Sub
            End If
        End If

        btnCreateLineVideos.Enabled = False
        My.Application.DoEvents()

        Dim sVideoFolderPath As String = Path.Combine(sFolderPath, sFileName & "-SmallVideos")
        If System.IO.Directory.Exists(sVideoFolderPath) = False Then
            System.IO.Directory.CreateDirectory(sVideoFolderPath)
        End If

        Dim sChapterName As String = txtFirstChapterName.Text
        'Dim oExtraLineImages As New ArrayList()
        Dim iFileCount As Integer = 0
        Dim iRow As Integer = 0
        Dim iBlankCount As Integer = 0
        Dim iChapterCount As Integer = 1

        ProgressBar1.Visible = True
        ProgressBar1.Maximum = iRows
        lbCount.Visible = True

        Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
        Dim sLine As String = oStreamReader.ReadLine()
        Do Until sLine Is Nothing
            iRow += 1

            If iBlankCount = 2 AndAlso Trim(sLine) <> "" Then
                Dim sSilenceFile As String = CheckForSilence(sLine)
                If sSilenceFile <> "" Then
                    sLine = Replace(sLine, "{{" & sSilenceFile & "}}", "")
                End If

                sChapterName = PadFileName(Trim(StrConv(sLine, VbStrConv.ProperCase)))

                'oExtraLineImages = New ArrayList() 'do not show line images from the previous chapter
                iChapterCount += 1
            End If

            Dim sChapterName2 As String = Microsoft.VisualBasic.Right("000" & iChapterCount, 3) & " " & Microsoft.VisualBasic.Left(sChapterName, 100)
            Dim sChapterFolderPath As String = Path.Combine(sVideoFolderPath, sChapterName2)
            Dim sSrcFileBase As String = Microsoft.VisualBasic.Right("000000" & iRow, iMaxSize)
            Dim sSrcFilePath As String = Path.Combine(sSrcFolderPath, sSrcFileBase & ".mp3")

            If IO.File.Exists(sSrcFilePath) Then

                iFileCount += 1
                ProgressBar1.Value = iFileCount
                lbCount.Text = iFileCount & "/" & iRows
                My.Application.DoEvents()

                If bStop Then
                    bStop = False
                    MsgBox("Stopped Processing at row " & iFileCount)
                    Exit Do
                End If

                If System.IO.Directory.Exists(sChapterFolderPath) = False Then
                    System.IO.Directory.CreateDirectory(sChapterFolderPath)
                End If

                Dim sOutputFileName As String = Path.GetFileNameWithoutExtension(sSrcFilePath) & ".mp4"
                Dim sOutputFilePath As String = Path.Combine(sChapterFolderPath, sOutputFileName)

                Dim bottomMargin As Single = txtBottomMargin.Text

                Dim oInputFileInfo As New IO.FileInfo(sSrcFilePath)
                Dim bSilenceFile As Boolean = oInputFileInfo.Length < 10000
                Dim sImageText As String = Path.GetFileNameWithoutExtension(sSrcFilePath)
                Dim sLineImageFilePath As String = ""

                If oLineImages.ContainsKey(sImageText) Then
                    sLineImageFilePath = oLineImages(sImageText)
                    If sLineImageFilePath.IndexOf(",") <> -1 Then
                        'multiple images referenced by the same line: show the first image now 
                        'show the remaining in later lines thet do no reference images
                        Dim oList As String() = sLineImageFilePath.Split(",")
                        sLineImageFilePath = oList(0)
                        'For i As Integer = 1 To oList.Length - 1
                        '    oExtraLineImages.Add(oList(i))
                        'Next
                    End If

                    'ElseIf oExtraLineImages.Count > 0 Then
                    '    sLineImageFilePath = oExtraLineImages(0)
                    '    If bSilenceFile = False Then
                    '        oExtraLineImages.RemoveAt(0)
                    '    End If
                End If

                If IO.File.Exists(sOutputFilePath) = False Then

                    Dim sInputFilePath2 As String = ""
                    Dim sTempImageFilePath As String = ""

                    If sLineImageFilePath <> "" Then
                        sInputFilePath2 = sLineImageFilePath

                    ElseIf chkImageText.Checked And sImageFilePath <> "" Then
                        Try
                            'Try to add text image
                            sTempImageFilePath = IO.Path.Combine(GetTempFolder(), GetGuidFileName("png"))

                            AddTextToImage(sImageFilePath, sChapterName, sTempImageFilePath, bottomMargin)
                            sInputFilePath2 = sTempImageFilePath
                        Catch ex As Exception
                            'Ignore if failed
                            sInputFilePath2 = sImageFilePath
                        End Try
                    ElseIf sImageFilePath <> "" Then
                        sInputFilePath2 = sImageFilePath
                    Else
                        sTempImageFilePath = IO.Path.Combine(GetTempFolder(), GetGuidFileName("png"))
                        AddTextToImage(sImageFilePath, PadImageText(sChapterName), sTempImageFilePath, bottomMargin)
                        sInputFilePath2 = sTempImageFilePath
                    End If

                    Dim sArguments As String = "-loop 1 -i """ & sInputFilePath2 & """ -i """ & sSrcFilePath & """ -c:v libx264 -vf ""pad=ceil(iw/2)*2:ceil(ih/2)*2"" -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -shortest """ & sOutputFilePath & """"

                    'If sLineImageFilePath <> "" Then
                    '    Dim iSeconds As Integer = 5
                    '    sArguments = "-loop 1 -i """ & sInputFilePath2 & """ -i """ & sSrcFilePath & """ -c:v libx264 -vf ""pad=ceil(iw/2)*2:ceil(ih/2)*2"" -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -t " & iSeconds & " """ & sOutputFilePath & """"
                    'End If

                    Dim oProcess As New System.Diagnostics.Process()
                    Dim startInfo As New System.Diagnostics.ProcessStartInfo()
                    startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
                    startInfo.FileName = sFfmpegFile
                    startInfo.Arguments = sArguments
                    oProcess.StartInfo = startInfo
                    oProcess.Start()
                    oProcess.WaitForExit()

                    If IO.File.Exists(sOutputFilePath) = False Then
                        Clipboard.SetText(sFfmpegFile & " " & sArguments)
                        MessageBox.Show("Video file was not created: " & sOutputFilePath & ". Command text is copied to Clipboard.")
                    Else
                        Dim oFileInfo As New FileInfo(sOutputFilePath)
                        If oFileInfo.Length < 10 Then
                            Clipboard.SetText(sFfmpegFile & " " & sArguments)
                            MessageBox.Show("Video file was created but is blank: " & sOutputFilePath & ". Command text is copied to Clipboard.")
                        End If
                    End If

                    If IO.File.Exists(sTempImageFilePath) Then
                        'Cleanup
                        Try
                            IO.File.Delete(sTempImageFilePath)
                        Catch ex As Exception
                            'Ignore
                        End Try
                    End If

                End If

            End If

            If Trim(sLine) = "" Then
                iBlankCount += 1
            Else
                iBlankCount = 0
            End If

            sLine = oStreamReader.ReadLine()
        Loop

        oStreamReader.Close()

        ResetProgressBar()
        btnCreateLineVideos.Enabled = True

        'Create Test.htm file in each sub-folder
        Dim oFolders As String() = System.IO.Directory.GetDirectories(sVideoFolderPath)
        For Each sSubFolder As String In oFolders
            CreateTextFile(sSubFolder)
        Next

        MsgBox("Done")

    End Sub

    Private Sub btnMakeVideos_Click(sender As Object, e As EventArgs) Handles btnMakeVideos.Click

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
            txtSrcFile.Text = ""
            MsgBox("Source text file Is blank")
            Exit Sub
        End If

        Dim sImageFilePath As String = txtImageFile.Text
        'If sImageFilePath = "" OrElse IO.File.Exists(sImageFilePath) = False Then
        '    txtImageFile.Text = ""
        '    MsgBox("Image file Is blank")
        '    Exit Sub
        'End If

        'Dim sVolume As String = "-2.5"
        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)

        Dim sChaptersFolderPath As String = Path.Combine(sFolderPath, sFileName & "-Chapters")
        If System.IO.Directory.Exists(sChaptersFolderPath) = False Then
            MsgBox("Chapters folders is missing: " & sChaptersFolderPath)
            Exit Sub
        End If

        Dim sFfmpegFile As String = GetFfmpegFile()
        If IO.File.Exists(sFfmpegFile) = False Then
            MsgBox("ffmpeg.exe file is missing: " & sFfmpegFile)
            Exit Sub
        End If

        If MsgBox("Creating video files might take hours.  " &
                "A video mp4 file will be created for each chapter file.  " &
                "Existing video files will be skipped.  " &
              "  Are you sure you want to do this?", vbYesNo) <> vbYes Then
            Exit Sub
        End If

        btnMakeVideos.Enabled = False
        My.Application.DoEvents()

        Dim sVideoFolderPath As String = Path.Combine(sFolderPath, sFileName & "-Videos")
        If System.IO.Directory.Exists(sVideoFolderPath) = False Then
            System.IO.Directory.CreateDirectory(sVideoFolderPath)
        End If

        Dim oFiles As String() = System.IO.Directory.GetFiles(sChaptersFolderPath)
        Dim iFileCount As Integer = 0
        ProgressBar1.Visible = True
        ProgressBar1.Maximum = oFiles.Length

        Dim oNoImageChapters As Hashtable = GetNoImageChapters()

        For Each sInputFilePath As String In oFiles
            iFileCount += 1
            ProgressBar1.Value = iFileCount

            My.Application.DoEvents()
            If bStop Then
                bStop = False
                MsgBox("Stopped Processing at row " & iFileCount)
                Exit For
            End If

            Dim bCanInclude As Boolean = True
            If oImageList.Count > 0 Then
                Dim sChapterFileName As String = Path.GetFileNameWithoutExtension(sInputFilePath)
                If oNoImageChapters.Contains(sChapterFileName) = False Then
                    bCanInclude = False
                End If
            End If

            Dim sOutputFileName As String = Path.GetFileNameWithoutExtension(sInputFilePath) & ".mp4"
            Dim sOutputFilePath As String = Path.Combine(sVideoFolderPath, sOutputFileName)

            'If IO.File.Exists(sOutputFilePath) Then
            '    IO.File.Delete(sOutputFilePath)
            'End If

            Dim bottomMargin As Single = txtBottomMargin.Text

            If IO.File.Exists(sOutputFilePath) = False AndAlso bCanInclude Then

                Dim sInputFilePath2 As String = sImageFilePath
                Dim sImageText As String = Path.GetFileNameWithoutExtension(sInputFilePath)
                Dim sTempImageFilePath As String = ""

                If chkImageText.Checked Then
                    Try
                        'Try to add text image
                        sTempImageFilePath = IO.Path.Combine(GetTempFolder(), GetGuidFileName("png"))
                        'AddTextToImage(sImageFilePath, PadImageText(sImageText), sTempImageFilePath, bottomMargin)
                        sImageText = PadImageText(sImageText)
                        sImageText = StrConv(sImageText, VbStrConv.ProperCase)
                        AddTextToImage(sImageFilePath, sImageText, sTempImageFilePath, bottomMargin)
                        sInputFilePath2 = sTempImageFilePath
                    Catch ex As Exception
                        'Ignore if failed
                    End Try
                ElseIf sImageFilePath = "" Then
                    sTempImageFilePath = IO.Path.Combine(GetTempFolder(), GetGuidFileName("png"))
                    AddTextToImage(sImageFilePath, PadImageText(sImageText), sTempImageFilePath, bottomMargin)
                    sInputFilePath2 = sTempImageFilePath
                End If

                Dim sArguments As String = "-loop 1 -i """ & sInputFilePath2 & """ -i """ & sInputFilePath & """ -c:v libx264 -vf ""pad=ceil(iw/2)*2:ceil(ih/2)*2"" -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -shortest """ & sOutputFilePath & """"
                Dim oProcess As New System.Diagnostics.Process()
                Dim startInfo As New System.Diagnostics.ProcessStartInfo()
                startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized
                startInfo.FileName = sFfmpegFile
                startInfo.Arguments = sArguments
                oProcess.StartInfo = startInfo
                oProcess.Start()
                oProcess.WaitForExit()

                If IO.File.Exists(sOutputFilePath) = False Then
                    Clipboard.SetText(sFfmpegFile & " " & sArguments)
                    MessageBox.Show("Video file was not created: " & sOutputFilePath & ". Command text is copied to Clipboard.")
                Else
                    Dim oFileInfo As New FileInfo(sOutputFilePath)
                    If oFileInfo.Length < 10 Then
                        Clipboard.SetText(sFfmpegFile & " " & sArguments)
                        MessageBox.Show("Video file was created but is blank: " & sOutputFilePath & ". Command text is copied to Clipboard.")
                    End If
                End If

                If IO.File.Exists(sTempImageFilePath) Then
                    'Cleanup
                    Try
                        IO.File.Delete(sTempImageFilePath)
                    Catch ex As Exception
                        'Ignore
                    End Try
                End If

            End If
        Next

        ProgressBar1.Visible = False
        btnMakeVideos.Enabled = True
        MsgBox("Done")

    End Sub

    Sub AddTextToImage(ByVal imagePath As String, ByVal text As String, ByVal savePath As String, ByVal bottomMargin As Single)

        Dim fontSize As Single = txtFontSize.Text

        ' Create a new blank image if the imagePath is empty
        Dim img As Image
        If String.IsNullOrEmpty(imagePath) Then
            img = New Bitmap(800, 800)
            Using g As Graphics = System.Drawing.Graphics.FromImage(img)
                g.Clear(Color.White) ' Set the background color to white if there is no image
            End Using
        Else
            ' Load the existing image
            img = Image.FromFile(imagePath)
        End If

        ' Create a Graphics object from the image
        Dim graphics As Graphics = Graphics.FromImage(img)

        ' Create a brush for the text
        Dim brush As New SolidBrush(btnTextColor.BackColor)

        ' Create a font for the text with the specified size
        Dim font As New Font("Arial", fontSize)

        Dim iLeftMargin As Integer = 0
        If txtLeftMargin.Text <> "" Then
            iLeftMargin = txtLeftMargin.Text
        End If

        ' Set up the text layout rectangle
        Dim layoutRect As New RectangleF(iLeftMargin, 0, img.Width - (iLeftMargin * 2), img.Height)

        ' Create a StringFormat object to specify text formatting
        Dim stringFormat As New StringFormat()
        stringFormat.Alignment = StringAlignment.Near
        stringFormat.LineAlignment = StringAlignment.Near
        stringFormat.FormatFlags = StringFormatFlags.LineLimit
        stringFormat.Trimming = StringTrimming.Word

        ' Measure the size of the text
        Dim textSize As SizeF = graphics.MeasureString(text, font, layoutRect.Size, stringFormat)

        ' Adjust bottomMargin if the text would go off the bottom of the image
        If bottomMargin + textSize.Height > img.Height Then
            bottomMargin = img.Height - textSize.Height - 10 ' Adjust with a small margin
        End If

        ' Define the text rectangle with the exact size of the text
        Dim textRect As New RectangleF(iLeftMargin, img.Height - textSize.Height - bottomMargin, textSize.Width, textSize.Height)

        ' Fill the rectangle with the background color
        Dim bgColorBrush As New SolidBrush(btnBgColor.BackColor)
        graphics.FillRectangle(bgColorBrush, textRect)

        ' Draw the string onto the image within the text rectangle
        graphics.DrawString(text, font, brush, textRect, stringFormat)

        ' Save the image
        img.Save(savePath, System.Drawing.Imaging.ImageFormat.Png)

        ' Clean up
        brush.Dispose()
        bgColorBrush.Dispose()
        graphics.Dispose()
        img.Dispose()
    End Sub

    Sub MergeMp4Files(ByVal sVideoFolderPath As String, ByVal sOutputFilePath As String, ByVal sFfmpegFile As String, iFileNameLength As Integer)

        Dim sFilePath As String = txtSrcFile.Text
        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sTempFolderPath As String = Path.Combine(sFolderPath, sFileName & "_one_video_" & DateTime.Now.ToString("yyyyMM_ddHHmmss"))

        IO.Directory.CreateDirectory(sTempFolderPath)

        'Convert .mp4 to .ts files
        ConverMp4toTs(sVideoFolderPath, sTempFolderPath, sFfmpegFile)

        'Wait
        System.Threading.Thread.Sleep(100)

        'Merge TS Files
        MergeTsFiles(sTempFolderPath, sOutputFilePath, sFfmpegFile)

        System.Threading.Thread.Sleep(1000)

        Try
            EmptyFolder(sTempFolderPath)
            IO.Directory.Delete(sTempFolderPath)
        Catch ex As Exception
            MsgBox("Could not empty folder " & sTempFolderPath)
            Exit Sub
        End Try

    End Sub

    Private Sub btnMakeVideo_Click(sender As Object, e As EventArgs) Handles btnMakeVideo.Click

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
            MsgBox("Source text file Is blank")
            Exit Sub
        End If

        Dim sFfmpegFile As String = GetFfmpegFile()
        If IO.File.Exists(sFfmpegFile) = False Then
            MsgBox("ffmpeg.exe file is missing: " & sFfmpegFile)
            Exit Sub
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sOutputFilePath As String = sFolderPath & "\" & sFileName & ".mp4"

        If IO.File.Exists(sOutputFilePath) Then
            MsgBox("Single video already exists. If you want to re-create it please delete it manually: " & sOutputFilePath)
            Exit Sub
        End If

        Dim sVideoFolderPath As String = Path.Combine(sFolderPath, sFileName & "-Videos")
        If System.IO.Directory.Exists(sVideoFolderPath) AndAlso System.IO.Directory.GetFiles(sVideoFolderPath).Length > 1 Then
            'Chapter MP4 already exists - merge them instead of creating new mp4 file
            MergeMp4Files(sVideoFolderPath, sOutputFilePath, sFfmpegFile, 3)
            Exit Sub
        End If

        Dim sImageFilePath As String = txtImageFile.Text
        If sImageFilePath = "" Then
            MsgBox("Image file Is blank")
            Exit Sub
        End If

        Dim sInputFilePath As String = sFolderPath & "\" & sFileName & ".mp3"
        If System.IO.File.Exists(sInputFilePath) = False Then
            MsgBox("MP3 file is is missing " & sInputFilePath)
            Exit Sub
        End If

        Dim oProcess As New System.Diagnostics.Process()
        Dim startInfo As New System.Diagnostics.ProcessStartInfo()
        startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Maximized
        startInfo.FileName = sFfmpegFile
        startInfo.Arguments = "-loop 1 -i """ & sImageFilePath & """ -i """ & sInputFilePath & """ -c:v libx264 -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -shortest """ & sOutputFilePath & """"
        oProcess.StartInfo = startInfo
        oProcess.Start()
        oProcess.WaitForExit()

        MsgBox("Done")
    End Sub

    Private Function TrimAlbum(ByVal s As String)
        If s.Length > 30 Then
            Return s.Substring(0, 30)
        Else
            Return s
        End If
    End Function

    Private Sub EmptyFolder(ByVal sFolder As String)

        For Each sFolderPath As String In IO.Directory.GetDirectories(sFolder)
            System.IO.Directory.Delete(sFolderPath, True)
        Next

        For Each sFilePath As String In IO.Directory.GetFiles(sFolder)
            File.Delete(sFilePath)
        Next

    End Sub

    Public Function PadFileName(ByVal s As String) As String
        s = Replace(s, "<", "")
        s = Replace(s, ">", "")
        s = Replace(s, ":", "-")
        s = Replace(s, """", "")
        s = Replace(s, "/", "")
        s = Replace(s, "\", "")
        s = Replace(s, "?", "")
        s = Replace(s, "'", "")
        s = Replace(s, ChrW(65533), "")
        's = Replace(s, " ", "_")
        Return Replace(s, "*", "")
    End Function

    Private Sub DataGridView1_CellClick(sender As Object, e As DataGridViewCellEventArgs) Handles DataGridView1.CellClick
        SetupLineText()
    End Sub

    Private Sub DataGridView1_KeyUp(sender As Object, e As KeyEventArgs) Handles DataGridView1.KeyUp
        SetupLineText()
        If chkPlayOnKeyUp.Checked Then
            Dim sFilePath As String = GetSelectedFielPath()
            If sFilePath <> "" Then
                PlaySound(sFilePath)
            End If
        End If
    End Sub

    Function GetSelectedRowIndex()
        If DataGridView1.SelectedRows.Count > 0 Then
            Return DataGridView1.SelectedRows(0).Index
        ElseIf DataGridView1.SelectedCells.Count > 0 Then
            Return DataGridView1.SelectedCells(0).RowIndex
        End If
        Return -1
    End Function

    Sub SetupLineText()
        Dim iSelectedRowIndex As Integer = GetSelectedRowIndex()
        If iSelectedRowIndex <> -1 Then
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iSelectedRowIndex)
            txtLine.Text = oRow.Cells("Text").Value
            Me.Text = "Audio Book Creator - " & oRow.Cells("Name").Value
        Else
            Me.Text = "Audio Book Creator"
        End If
    End Sub

    Function GetSelectedFielPath() As String
        Dim iSelectedRowIndex As Integer = GetSelectedRowIndex()
        If iSelectedRowIndex <> -1 Then
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iSelectedRowIndex)
            Return oRow.Cells("FilePath").Value
        End If
        Return ""
    End Function

    Private Sub txtLine_TextChanged(sender As Object, e As EventArgs) Handles txtLine.TextChanged
        Dim iSelectedRowIndex As Integer = GetSelectedRowIndex()
        If iSelectedRowIndex <> -1 Then
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iSelectedRowIndex)
            oRow.Cells("Text").Value = txtLine.Text
            oRow.Cells("TextSize").Value = Len(txtLine.Text)
            btnSave.Visible = True
        End If
    End Sub

    Private Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
        SaveTextFile()
    End Sub

    Sub SaveTextFile()
        Dim sFilePath As String = txtSrcFile.Text
        Dim oEncoding As System.Text.Encoding = System.Text.Encoding.ASCII
        Dim sBackupFilePath As String = ""

        If System.IO.File.Exists(sFilePath) Then
            oEncoding = DetectEncoding(sFilePath)
            Dim sBackupFileName As String = Path.GetFileNameWithoutExtension(sFilePath) & "_" & DateTime.Now.ToString("yyyyMM_ddHHmmss") & Path.GetExtension(sFilePath)
            sBackupFilePath = Path.Combine(Path.GetDirectoryName(sFilePath), sBackupFileName)
            File.Move(sFilePath, sBackupFilePath)
        End If

        Dim sw As New StreamWriter(sFilePath, False, oEncoding)
        For iRow = 0 To DataGridView1.RowCount - 1
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
            If oRow.IsNewRow = False Then
                Dim sText As String = oRow.Cells("Text").Value
                Dim sName As String = oRow.Cells("Name").Value
                sw.WriteLine(sText)
            End If
        Next

        sw.Close()

        If sBackupFilePath <> "" Then
            If chkBackupFile.Checked Then
                Dim sBackupFolder As String = Path.Combine(Path.GetDirectoryName(sFilePath), Path.GetFileNameWithoutExtension(sFilePath) & "_backup")
                If IO.Directory.Exists(sBackupFolder) = False Then
                    IO.Directory.CreateDirectory(sBackupFolder)
                End If
                Dim sNewBackupFilePath = Path.Combine(sBackupFolder, Path.GetFileName(sBackupFilePath))
                File.Move(sBackupFilePath, sNewBackupFilePath)
            Else
                File.Delete(sBackupFilePath)
            End If
        End If

        btnSave.Visible = False
    End Sub

    Function DetectEncoding(filePath As String) As System.Text.Encoding
        Dim encoding As System.Text.Encoding = System.Text.Encoding.Default ' Fallback to the default encoding if BOM is not found

        Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read)
            If fs.Length >= 2 Then
                Dim bom(3) As Byte
                fs.Read(bom, 0, 3)

                If bom(0) = &HEF AndAlso bom(1) = &HBB AndAlso bom(2) = &HBF Then
                    encoding = System.Text.Encoding.UTF8
                ElseIf bom(0) = &HFF AndAlso bom(1) = &HFE Then
                    encoding = System.Text.Encoding.Unicode
                ElseIf bom(0) = &HFE AndAlso bom(1) = &HFF Then
                    encoding = System.Text.Encoding.BigEndianUnicode
                ElseIf bom(0) = &H0 AndAlso bom(1) = &H0 AndAlso bom(2) = &HFE AndAlso bom(3) = &HFF Then
                    encoding = System.Text.Encoding.UTF32
                End If
            End If
        End Using

        Return encoding
    End Function

    Private Sub btnSilence_Click(sender As Object, e As EventArgs) Handles btnSilence.Click
        txtLine.Text = "{{" & selSilence.SelectedItem.ToString() & ".mp3}}"
        btnSave.Visible = True
    End Sub

    Private Sub txtLine_MouseWheel(sender As Object, e As MouseEventArgs) Handles txtLine.MouseWheel

        If Control.ModifierKeys = Keys.Control Then
            Dim currentSize As Single = txtLine.Font.Size
            Dim newSize As Single

            If e.Delta > 0 Then
                ' Mouse wheel was moved up, increase font size
                newSize = currentSize + 1
            Else
                ' Mouse wheel was moved down, decrease font size
                newSize = Math.Max(currentSize - 1, 1)
            End If

            txtLine.Font = New Font(txtLine.Font.FontFamily, newSize, txtLine.Font.Style)
        End If

    End Sub

    Private Sub btnMerge_Click(sender As Object, e As EventArgs) Handles btnMerge.Click
        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
            MsgBox("Text file is blank")
            Exit Sub
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sChaptersFolderPath As String = Path.Combine(sFolderPath, sFileName & "-Chapters")

        If IO.Directory.Exists(sChaptersFolderPath) = False Then
            MsgBox("Chapters folder does not exist: " & sChaptersFolderPath)
            Exit Sub
        End If

        Dim sDestFilePath As String = sFolderPath & "\" & sFileName & ".mp3"

        If IO.File.Exists(sDestFilePath) Then
            IO.File.Delete(sDestFilePath)
        End If

        MergeFolder(sChaptersFolderPath, sDestFilePath)

        Dim oMP3Info As New Monotic.Multimedia.MP3.MP3Info(sDestFilePath)
        txtText.Text = "Created Single MP3 File with Length: " & oMP3Info.Length

        MsgBox("Done")
    End Sub

    Private Sub DeleteFolder(ByVal sFolderPath As String)
        If IO.Directory.Exists(sFolderPath) = False Then
            Exit Sub
        End If

        Dim oFiles As String() = System.IO.Directory.GetFiles(sFolderPath)
        For Each sFile In oFiles
            IO.File.Delete(sFile)
        Next

        IO.Directory.Delete(sFolderPath)

    End Sub



    Private Sub btnYouTubeIndex_Click(sender As Object, e As EventArgs) Handles btnYouTubeIndex.Click

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" Then
            MsgBox("Text file is blank")
            Exit Sub
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sChaptersFolderPath As String = Path.Combine(sFolderPath, sFileName & "-Chapters")
        If System.IO.Directory.Exists(sChaptersFolderPath) = False Then
            MsgBox("Chapters folder does not exist: " & sChaptersFolderPath)
            Exit Sub
        End If

        Dim oForm As New frmYouTube
        oForm.sChaptersFolderPath = sChaptersFolderPath
        oForm.ShowDialog()
    End Sub

    Private Sub btnApiKeyShow_Click(sender As Object, e As EventArgs) Handles btnApiKeyShow.Click
        If txtApiKey.PasswordChar = "*" Then
            txtApiKey.PasswordChar = ""
        Else
            txtApiKey.PasswordChar = "*"
        End If
    End Sub

    Private Sub selHighlight_SelectedIndexChanged(sender As Object, e As EventArgs) Handles selHighlight.SelectedIndexChanged
        DataGridColor()
    End Sub

    Private Sub urlApiKey_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles urlApiKey.LinkClicked
        Process.Start(New ProcessStartInfo("https://console.cloud.google.com/apis/credentials?project=_"))
    End Sub

    Private Sub btnRenameDown_Click(sender As Object, e As EventArgs) Handles btnRenameDown.Click

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" OrElse IO.File.Exists(sFilePath) = False Then
            txtSrcFile.Text = ""
            MsgBox("Text file is blank")
            Exit Sub
        End If

        Dim iRows As Integer = GetFileRowsCount(sFilePath)
        If iRows = 0 Then
            Exit Sub
        End If

        Dim iMaxSize As Integer = iRows.ToString().Length
        Dim sFolderPath As String = Path.GetDirectoryName(txtSrcFile.Text)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sDestFolderPath As String = Path.Combine(sFolderPath, sFileName)

        Dim iSelectedRowIndex As Integer = GetSelectedRowIndex()
        If iSelectedRowIndex = -1 Then
            Exit Sub
        End If

        If MsgBox("Rename file " & (iSelectedRowIndex + 1) & " and subsequent files by + 1?", MsgBoxStyle.YesNo, " file") <> vbYes Then
            Exit Sub
        End If

        PlaySoundStop()

        For iRow = DataGridView1.RowCount - 1 To iSelectedRowIndex Step -1
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
            If oRow.IsNewRow = False Then
                Dim sSrcFilePath As String = oRow.Cells("FilePath").Value
                If sSrcFilePath <> "" AndAlso File.Exists(sSrcFilePath) Then
                    Dim sDestFileBase As String = Microsoft.VisualBasic.Right("000000" & (iRow + 2), iMaxSize)
                    Dim sDestFilePath As String = Path.Combine(sDestFolderPath, sDestFileBase & ".mp3")

                    Try
                        IO.File.Move(sSrcFilePath, sDestFilePath)
                    Catch ex As Exception
                        MsgBox(ex.Message)
                    End Try

                End If
            End If
        Next

        MsgBox("Done")

    End Sub

    Private Sub btnTextColor_Click(sender As Object, e As EventArgs) Handles btnTextColor.Click
        Dim cDialog As New ColorDialog()
        If (cDialog.ShowDialog() = DialogResult.OK) Then
            btnTextColor.BackColor = cDialog.Color
        End If
    End Sub

    Private Sub btnBgColor_Click(sender As Object, e As EventArgs) Handles btnBgColor.Click
        Dim cDialog As New ColorDialog()
        If (cDialog.ShowDialog() = DialogResult.OK) Then
            btnBgColor.BackColor = cDialog.Color
        End If
    End Sub

    Private Sub chkImageText_CheckedChanged(sender As Object, e As EventArgs) Handles chkImageText.CheckedChanged
        txtLeftMargin.Enabled = chkImageText.Checked
        txtBottomMargin.Enabled = chkImageText.Checked
        btnBgColor.Enabled = chkImageText.Checked
        btnTextColor.Enabled = chkImageText.Checked
    End Sub

    Private Sub btnVideoTest_Click(sender As Object, e As EventArgs) Handles btnVideoTest.Click
        Dim bottomMargin As Single = txtBottomMargin.Text
        Dim sImageFilePath As String = txtImageFile.Text
        Dim oImageTexts As String() = {"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
            "Lorem ipsum dolor sit amet",
            "Chapter 1",
            "003 Глава 2. Дело против чувства вины - Руководство для родителей по поведенческой генетике."}
        Dim i As Integer = CInt(oImageTexts.Length * Rnd())
        If i > oImageTexts.Length - 1 Then i = oImageTexts.Length - 1
        Dim sImageText As String = oImageTexts(i)
        Dim sTempImageFilePath As String = IO.Path.Combine(GetTempFolder(), GetGuidFileName("png"))
        AddTextToImage(sImageFilePath, sImageText, sTempImageFilePath, bottomMargin)
        System.Diagnostics.Process.Start(sTempImageFilePath)
    End Sub

    Private Sub btnProcessChapter_Click(sender As Object, e As EventArgs) Handles btnProcessChapter.Click
        If MsgBox("Are you sure you want to process lines from selected down until a blank line?", vbYesNo) <> vbYes Then
            Exit Sub
        End If

        Dim iSelectedRowIndex As Integer = GetSelectedRowIndex()
        If iSelectedRowIndex = -1 Then
            Exit Sub
        End If

        btnProcessChapter.Enabled = False
        My.Application.DoEvents()

        Dim i As Integer = iSelectedRowIndex

        Do
            Dim oRow As DataGridViewRow = DataGridView1.Rows(i)
            Dim sText As String = Trim(oRow.Cells("Text").Value & "")
            If sText = "" OrElse i > 100000 Then
                Exit Do
            Else
                ProcessTextFile(i + 1)
                i += 1
            End If
        Loop

        UpdateFileGrid()
        btnProcessChapter.Enabled = True
        MsgBox("Done at line: " & (i + 1))

    End Sub

    Sub ConverMp4toTs(sSrcFolder As String, sDestChapterFolderPath As String, sFfmpegFile As String)

        For Each sSrcFilePath As String In IO.Directory.GetFiles(sSrcFolder)
            Dim sDestFilePath As String = Path.Combine(sDestChapterFolderPath, Path.GetFileNameWithoutExtension(sSrcFilePath) & ".ts")
            If IO.File.Exists(sDestFilePath) = False Then
                Dim oProcess As New System.Diagnostics.Process()
                Dim startInfo As New System.Diagnostics.ProcessStartInfo()
                startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
                startInfo.FileName = sFfmpegFile
                startInfo.Arguments = " -i """ & sSrcFilePath & """ -c copy -bsf:v h264_mp4toannexb -f mpegts """ & sDestFilePath & """"
                oProcess.StartInfo = startInfo
                oProcess.Start()
                oProcess.WaitForExit()
            End If
        Next

    End Sub

    Sub MergeTsFiles(sFolder, sDestFilePath, sFfmpegFile)

        If IO.File.Exists(sDestFilePath) Then
            Exit Sub
        End If

        Dim sTextFilePath As String = Path.Combine(sFolder, "Files.txt")
        Dim sw As New StreamWriter(sTextFilePath, False)
        For Each sPath As String In IO.Directory.GetFiles(sFolder)
            If Path.GetExtension(sPath) = ".ts" Then
                sw.WriteLine("file '" & Path.GetFileName(sPath) & "'")
            End If
        Next
        sw.Close()

        Dim oProcess As New System.Diagnostics.Process()
        Dim startInfo As New System.Diagnostics.ProcessStartInfo()
        startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
        startInfo.FileName = sFfmpegFile
        startInfo.Arguments = "-f concat -safe 0 -i """ & sTextFilePath & """ -c:v libx264 -c:a aac """ & sDestFilePath & """"
        oProcess.StartInfo = startInfo
        oProcess.Start()
        oProcess.WaitForExit()

        System.Threading.Thread.Sleep(1000)
    End Sub

    Private Sub btnMergeToChapters_Click(sender As Object, e As EventArgs) Handles btnMergeToChapters.Click

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" Then
            MsgBox("Text file is blank")
            Exit Sub
        End If

        If IO.File.Exists(sFilePath) = False Then
            txtSrcFile.Text = ""
            MsgBox("Text file is blank")
            Exit Sub
        End If

        Dim sFfmpegFile As String = GetFfmpegFile()
        If IO.File.Exists(sFfmpegFile) = False Then
            MsgBox("ffmpeg.exe file is missing: " & sFfmpegFile)
            Exit Sub
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)

        Dim sSrcFolderPath As String = Path.Combine(sFolderPath, sFileName & "-SmallVideos")
        If System.IO.Directory.Exists(sSrcFolderPath) = False Then
            MsgBox("Source folder Is blank: " & sSrcFolderPath)
            Exit Sub
        End If

        Dim sDstFolderPath As String = Path.Combine(sFolderPath, sFileName & "-LineChapters")
        If System.IO.Directory.Exists(sDstFolderPath) = False Then
            System.IO.Directory.CreateDirectory(sDstFolderPath)
        End If

        btnMergeToChapters.Enabled = False
        My.Application.DoEvents()

        Dim oFolderList As String() = IO.Directory.GetDirectories(sSrcFolderPath)
        Dim iFolderCount As Integer = 0
        ProgressBar1.Visible = True
        ProgressBar1.Maximum = oFolderList.Length
        lbCount.Visible = True

        'Convert .mp4 to .ts files
        For Each sSubFolder As String In oFolderList
            iFolderCount += iFolderCount
            ProgressBar1.Value = iFolderCount
            lbCount.Text = iFolderCount & "/" & oFolderList.Length
            My.Application.DoEvents()

            Dim sChapterName As String = IO.Path.GetFileName(sSubFolder)
            Dim sDestChapterFolderPath As String = Path.Combine(sDstFolderPath, sChapterName)

            If System.IO.Directory.Exists(sDestChapterFolderPath) = False Then
                System.IO.Directory.CreateDirectory(sDestChapterFolderPath)
            End If

            ConverMp4toTs(sSubFolder, sDestChapterFolderPath, sFfmpegFile)
        Next

        'Wait
        System.Threading.Thread.Sleep(100)

        oFolderList = IO.Directory.GetDirectories(sDstFolderPath)
        iFolderCount = 0
        ProgressBar1.Maximum = oFolderList.Length

        'Merge TS Files
        For Each sSubFolder As String In IO.Directory.GetDirectories(sDstFolderPath)
            iFolderCount += iFolderCount
            ProgressBar1.Value = iFolderCount
            lbCount.Text = iFolderCount & "/" & oFolderList.Length
            My.Application.DoEvents()

            Dim sDestFilePath As String = sSubFolder & ".mp4"
            MergeTsFiles(sSubFolder, sDestFilePath, sFfmpegFile)
        Next

        ProgressBar1.Visible = False
        lbCount.Visible = False

        'Delete .ts files
        If MsgBox("Done.  Do you want to delete temp (.ts) folders?  You can delete them manully later, all or induvidually.", vbYesNo) = vbYes Then
            For Each sSubFolder As String In IO.Directory.GetDirectories(sDstFolderPath)
                System.IO.Directory.Delete(sSubFolder, True)
            Next
        End If

        btnMergeToChapters.Enabled = True
        MsgBox("Done")
    End Sub

    Private Function GetHashFromFile(ByVal sFilePath As String) As Hashtable
        Dim oHash As New Hashtable

        Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sFilePath)
        Dim sLine As String = oStreamReader.ReadLine()
        Do Until sLine Is Nothing
            If sLine.IndexOf(vbTab) <> -1 Then
                Dim sName As String = Split(sLine, vbTab)(0)
                Dim sText As String = Split(sLine, vbTab)(1)
                oHash(sName) = sText
            End If

            sLine = oStreamReader.ReadLine()
        Loop
        oStreamReader.Close()

        Return oHash
    End Function

    Private Sub btnAiSettings_Click(sender As Object, e As EventArgs) Handles btnAiSettings.Click
        SetAiSettings()
    End Sub

    Private Sub SetAiSettings()
        frmValidate.sAIService = sAIService
        frmValidate.sAnthropicKey = sAnthropicKey
        frmValidate.sOpenAiApiKey = sOpenAiApiKey
        frmValidate.sAIInstructions = sAIInstructions
        frmValidate.ShowDialog()

        If frmValidate.bCancel Then
            Exit Sub
        Else
            sAIService = frmValidate.sAIService
            sAnthropicKey = frmValidate.sAnthropicKey
            sOpenAiApiKey = frmValidate.sOpenAiApiKey
            sAIInstructions = frmValidate.sAIInstructions
        End If
    End Sub

    Private Sub btnValidate_Click(sender As Object, e As EventArgs) Handles btnValidate.Click

        If MsgBox("Process your text file against AI Service?", vbYesNo) <> vbYes Then
            Exit Sub
        End If

        If sAIInstructions = "" Then
            MsgBox("Instructions are not provided")
            SetAiSettings()
            Exit Sub
        End If

        If sAIService = "" Then
            MsgBox("Service is not provided")
            SetAiSettings()
            Exit Sub
        End If

        If Microsoft.VisualBasic.Left(sAIService, 4) <> "gpt-" Then
            If sAnthropicKey = "" Then
                MsgBox("Anthropic API key is not provided")
                SetAiSettings()
                Exit Sub
            End If
        Else 'OpenAI
            If sOpenAiApiKey = "" Then
                MsgBox("OpenAI API key is not provided")
                SetAiSettings()
                Exit Sub
            End If
        End If

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" Then
            MsgBox("Text file is not selected")
            Exit Sub
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sOutFilePath As String = Path.Combine(sFolderPath, sAiProcessFileName)
        Dim sw As StreamWriter
        Dim oHash As New Hashtable

        If IO.File.Exists(sOutFilePath) Then

            If MsgBox("AI Validate file (" & sAiProcessFileName & ") already exists.  " &
                      "Do you want to resume processing it?  " &
                      "If all lines have been processed no validation request will be sent to the AI service.", vbYesNo) <> vbYes Then
                Exit Sub
            End If

            oHash = GetHashFromFile(sOutFilePath)

            sw = New StreamWriter(sOutFilePath, True, System.Text.Encoding.UTF8)
        Else
            sw = New StreamWriter(sOutFilePath, False, System.Text.Encoding.UTF8)
        End If

        ProgressBar1.Visible = True
        ProgressBar1.Maximum = DataGridView1.RowCount
        lbCount.Visible = True

        For iRow = 0 To DataGridView1.RowCount - 1

            ProgressBar1.Value = iRow
            lbCount.Text = iRow & "/" & DataGridView1.RowCount
            My.Application.DoEvents()

            Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
            If oRow.IsNewRow = False Then
                Dim sName As String = DataGridView1.Rows(iRow).Cells("Name").Value & ""
                If oHash.ContainsKey(sName) = False Then
                    Dim sText As String = DataGridView1.Rows(iRow).Cells("Text").Value & ""
                    Dim sRet As String = "OK."

                    If Trim(sText) <> "" Then

                        For iTry As Integer = 1 To 100
                            Try

                                If Microsoft.VisualBasic.Left(sAIService, 4) <> "gpt-" Then
                                    sRet = SendAnthropicMsg(sAIInstructions & vbCrLf & vbCrLf & sText)

                                Else 'OpenAI
                                    sRet = SendOpenAiMsg(sAIInstructions & vbCrLf & vbCrLf & sText)
                                End If

                                Exit For
                            Catch ex As Exception
                                If ex.Message.ToLower().IndexOf("too many requests") Then
                                    'Sleep and Try again
                                    Threading.Thread.Sleep(1000 * iTry)
                                    If iTry >= 100 Then
                                        MsgBox(ex.Message)
                                        sRet = "Error: " & ex.Message
                                    End If
                                Else
                                    sRet = "Error: " & ex.Message
                                    Exit For
                                End If
                            End Try
                        Next

                        If sRet = "OK" Or sRet = "OK." Then
                            sRet = "OK"
                        Else
                            sRet = sRet
                        End If
                    End If

                    sRet = Replace(sRet, vbTab, " ")
                    sRet = Replace(sRet, vbCrLf, " ")
                    sRet = Replace(sRet, vbCr, " ")
                    sRet = Replace(sRet, vbLf, " ")
                    sw.WriteLine(sName & vbTab & sRet)
                End If
            End If
        Next

        sw.Close()

        ProgressBar1.Visible = False
        lbCount.Visible = False

        Process.Start("notepad.exe", """" & sOutFilePath & """")

    End Sub

    Private Sub btnAiOpenFile_Click(sender As Object, e As EventArgs) Handles btnAiOpenFile.Click
        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" Then
            MsgBox("Text file is not selected")
            Exit Sub
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sInFilePath As String = Path.Combine(sFolderPath, sAiProcessFileName)
        If IO.File.Exists(sInFilePath) = False Then
            Exit Sub
        End If

        Dim oHash As Hashtable = GetHashFromFile(sInFilePath)
        If oHash.Count = 0 Then
            Exit Sub
        End If

        Dim sOutFilePath As String = Path.Combine(sFolderPath, "Plain_" & sAiProcessFileName)
        If IO.File.Exists(sOutFilePath) Then
            IO.File.Delete(sOutFilePath)
        End If

        Dim sw As StreamWriter = New StreamWriter(sOutFilePath, True, System.Text.Encoding.UTF8)
        Dim iRows As Integer = GetFileRowsCount(sFilePath)
        Dim iMaxSize As Integer = iRows.ToString().Length

        For iRow = 1 To iRows
            Dim sSrcFileBase As String = Microsoft.VisualBasic.Right("000000" & iRow, iMaxSize)
            Dim sMsg As String = oHash(sSrcFileBase)
            If sMsg = "OK." Then
                sw.WriteLine("")
            Else
                sw.WriteLine(sMsg)
            End If
        Next

        sw.Close()

        Process.Start("notepad.exe", """" & sOutFilePath & """")

    End Sub

    Function SendOpenAiMsg(ByVal sQuestion As String) As String

        System.Net.ServicePointManager.SecurityProtocol =
           System.Net.SecurityProtocolType.Ssl3 Or
           System.Net.SecurityProtocolType.Tls12 Or
           System.Net.SecurityProtocolType.Tls11 Or
           System.Net.SecurityProtocolType.Tls

        Dim sUrl As String = "https://api.openai.com/v1/chat/completions"
        Dim request As HttpWebRequest = WebRequest.Create(sUrl)
        request.Method = "POST"
        request.ContentType = "application/json"
        request.Headers.Add("Authorization", "Bearer " & sOpenAiApiKey)

        'https://beta.openai.com/docs/api-reference/completions/create
        Dim data As String = "{"
        data += " ""model"":""" & sAIService & ""","
        data += " ""messages"": [{""role"":""user"", ""content"": """ & PadQuotes(sQuestion) & """}]"
        data += "}"

        Using streamWriter As New StreamWriter(request.GetRequestStream())
            streamWriter.Write(data)
            streamWriter.Flush()
            streamWriter.Close()
        End Using

        Dim response As HttpWebResponse = request.GetResponse()
        Dim streamReader As New StreamReader(response.GetResponseStream())
        Dim sJson As String = streamReader.ReadToEnd()

        Dim oJavaScriptSerializer As New System.Web.Script.Serialization.JavaScriptSerializer
        Dim oJson As Hashtable = oJavaScriptSerializer.Deserialize(Of Hashtable)(sJson)
        Return oJson("choices")(0)("message")("content")
    End Function

    Function SendAnthropicMsg(ByVal sQuestion As String) As String

        System.Net.ServicePointManager.SecurityProtocol =
           System.Net.SecurityProtocolType.Ssl3 Or
           System.Net.SecurityProtocolType.Tls12 Or
           System.Net.SecurityProtocolType.Tls11 Or
           System.Net.SecurityProtocolType.Tls

        Dim sUrl As String = "https://api.anthropic.com/v1/messages"
        Dim request As HttpWebRequest = WebRequest.Create(sUrl)
        request.Method = "POST"
        request.ContentType = "application/json"
        request.Headers.Add("x-api-key", sAnthropicKey)
        request.Headers.Add("anthropic-version", "2023-06-01")

        'https://docs.anthropic.com/en/api/messages
        'https://docs.anthropic.com/en/api/messages-examples
        Dim iMaxTokens As Integer = 1024
        Dim dTemperature As Double = 0.7

        Dim data As String = "{"
        data += """model"": """ & sAIService & ""","
        data += """messages"": [{""role"":""user"", ""content"": """ & PadQuotes(sQuestion) & """}],"
        data += """system"": ""You are Claude, an AI assistant created by Anthropic to be helpful, harmless, and honest."","
        data += """max_tokens"": " & iMaxTokens & ","
        data += """temperature"": " & dTemperature
        data += "}"

        Using streamWriter As New StreamWriter(request.GetRequestStream())
            streamWriter.Write(data)
            streamWriter.Flush()
            streamWriter.Close()
        End Using

        Dim response As HttpWebResponse = request.GetResponse()
        Dim streamReader As New StreamReader(response.GetResponseStream())
        Dim sJson As String = streamReader.ReadToEnd()

        Dim oJavaScriptSerializer As New System.Web.Script.Serialization.JavaScriptSerializer
        Dim oJson As Hashtable = oJavaScriptSerializer.Deserialize(Of Hashtable)(sJson)
        Return oJson("content")(0)("text")
    End Function

    Private Sub btnDeleteValid_Click(sender As Object, e As EventArgs) Handles btnDeleteValid.Click

        If MsgBox("Delete mp3 files that have AI correction suggestion?  " &
                  "This will alllow you to regenerate them by clicking 'Process Text File'", vbYesNo) <> vbYes Then
            Exit Sub
        End If

        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" Then
            Exit Sub
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sInFilePath As String = Path.Combine(sFolderPath, sAiProcessFileName)
        If IO.File.Exists(sInFilePath) = False Then
            MsgBox("Click AI Validate first.")
            Exit Sub
        End If

        Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
        Dim sDestFolderPath As String = Path.Combine(sFolderPath, sFileName)

        Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sInFilePath)
        Dim sLine As String = oStreamReader.ReadLine()
        Do Until sLine Is Nothing
            If sLine.IndexOf(vbTab) <> -1 Then
                Dim sName As String = Split(sLine, vbTab)(0)
                Dim sNew As String = Split(sLine, vbTab)(1)
                If sNew <> "OK" AndAlso sNew <> "OK." AndAlso sNew <> "" Then
                    Dim sMp3FilePath As String = Path.Combine(sDestFolderPath, sName & ".mp3")
                    If IO.File.Exists(sMp3FilePath) Then
                        IO.File.Delete(sMp3FilePath)
                    End If
                End If
            End If

            sLine = oStreamReader.ReadLine()
        Loop

        oStreamReader.Close()
    End Sub

    Private Sub btnReviewAI_Click(sender As Object, e As EventArgs) Handles btnReviewAI.Click
        Dim sFilePath As String = txtSrcFile.Text
        If sFilePath = "" Then
            Exit Sub
        End If

        Dim sFolderPath As String = Path.GetDirectoryName(sFilePath)
        Dim sInFilePath As String = Path.Combine(sFolderPath, sAiProcessFileName)
        If IO.File.Exists(sInFilePath) = False Then
            MsgBox("Click AI Validate first.")
            Exit Sub
        End If

        Dim oOld As New Hashtable
        For iRow = 0 To DataGridView1.RowCount - 1
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
            If oRow.IsNewRow = False Then
                Dim sName As String = DataGridView1.Rows(iRow).Cells("Name").Value & ""
                Dim sText As String = DataGridView1.Rows(iRow).Cells("Text").Value & ""
                oOld(sName) = sText
            End If
        Next

        Dim sOutFilePath As String = Path.Combine(sFolderPath, "Validate.hta")
        If IO.File.Exists(sOutFilePath) Then
            IO.File.Delete(sOutFilePath)
        End If

        Dim oNew As New Hashtable
        Dim sw As New Text.StringBuilder()
        sw.Append("<table border=1 cellspacing=0>")
        sw.Append("<thead><tr>" &
                  "<th>Select</th>" &
                  "<th>Line</th>" &
                  "<th nowrap>Change %</th>" &
                  "<th>&nbsp;</th>" &
                  "<th>Text</th>" &
                  "</tr></thead><tbody>")

        Dim oStreamReader As System.IO.StreamReader = GetStreamReader(sInFilePath)
        Dim sLine As String = oStreamReader.ReadLine()
        Do Until sLine Is Nothing
            If sLine.IndexOf(vbTab) <> -1 Then
                Dim sName As String = Split(sLine, vbTab)(0)
                Dim sNew As String = Split(sLine, vbTab)(1)
                Dim sOld As String = oOld(sName)

                If sNew <> "OK" AndAlso sNew <> "OK." AndAlso sNew <> "" AndAlso sNew <> sOld Then
                    Dim iPercentLengthChange As Integer = (CDbl(Len(sNew)) / CDbl(Len(sOld))) * 100

                    oNew(sName) = sNew

                    Dim sChecked As String = ""
                    Dim sRedCss As String = ""
                    If iPercentLengthChange > 90 And iPercentLengthChange < 110 Then
                        'seems to me valid AI response
                        sChecked = " checked "

                    Else
                        sRedCss = "background-color: red"
                    End If

                    sw.Append("<tr>" &
                                     "<td rowspan=3><input type='checkbox' name='chkBox' " & sChecked & " value='" & sName & "'></td>" &
                                     "<td rowspan=3>" & sName & "</td>" &
                                     "<td rowspan=3><span  style='" & sRedCss & "'>" & iPercentLengthChange & "</span></td>" &
                                     "<td>Old:</td><td>" & sOld & "</td></tr>")
                    sw.Append("<tr><td>New:</td><td>" & sNew & "</td></tr>")
                    sw.Append("<tr><td>Change:</td><td>" & CompareStrings(sOld, sNew) & "</td></tr>")
                    sw.Append("<tr><td colspan=5><hr/></td></tr>")
                End If

            End If

            sLine = oStreamReader.ReadLine()
        Loop

        oStreamReader.Close()

        sw.Append("</tbody></table>")

        Dim sHtml As String = sw.ToString() &
        "<p align=center>" &
        "<input type='button' value='Select All' onclick='SelectAll(True)'> " &
        "<input type='button' value='Unselect All' onclick='SelectAll(False)'> " &
        "<input type='button' value='Update Selected' onclick='Send()'> " &
        "<input type='button' value='Close' onclick='self.Close()'></p>"

        CreateHtaFile(sOutFilePath, sHtml, "AI Suggested Changes")
        File.SetAttributes(sOutFilePath, FileAttributes.Hidden)
        OpenHTAAndWait(sOutFilePath)

        If File.Exists(sOutFilePath) Then
            File.Delete(sOutFilePath)
        End If

        Dim oRet As Hashtable = ReadXmlFile(sOutFilePath & ".xml", "List", True)
        Dim sList As String = Trim(oRet("List") & "")
        If sList = "" Then
            Exit Sub
        End If

        Dim oSelected As New Hashtable
        For Each sRow As String In Split(sList, ",")
            oSelected(sRow) = True
        Next

        'Update Selected
        For iRow = 0 To DataGridView1.RowCount - 1
            Dim oRow As DataGridViewRow = DataGridView1.Rows(iRow)
            If oRow.IsNewRow = False Then
                Dim sName As String = DataGridView1.Rows(iRow).Cells("Name").Value & ""
                If oSelected.ContainsKey(sName) AndAlso oNew.ContainsKey(sName) Then
                    DataGridView1.Rows(iRow).Cells("Text").Value = oNew(sName)
                End If
            End If
        Next

        If MsgBox("Updated  " & oSelected.Count & " rows with AI suggestions.  " &
                  "Save it now? You can also save these changes later by clicking 'Save Text File'.", MsgBoxStyle.YesNo, " file") = vbYes Then
            SaveTextFile()
        End If

    End Sub


    Function ReadXmlFile(sFilePath As String, sFields As String, bDeleteAfterRead As Boolean) As Hashtable
        Dim oRet As New Hashtable

        ' Load return data from XML File
        If File.Exists(sFilePath) Then
            Dim oXml As New Xml.XmlDocument()
            oXml.Load(sFilePath)

            For Each sField As String In sFields.Split(","c)
                Dim oNode As Xml.XmlNode = oXml.SelectSingleNode("/root/" & sField.Trim())
                If oNode IsNot Nothing Then
                    oRet.Add(sField.Trim(), oNode.InnerText)
                End If
            Next

            If bDeleteAfterRead Then
                File.Delete(sFilePath)
            End If
        End If

        Return oRet
    End Function

    Sub CreateHtaFile(sHtaFilePath, sHtml, sTitle)

        If IO.File.Exists(sHtaFilePath) Then
            IO.File.Delete(sHtaFilePath)
        End If

        Dim f As StreamWriter = New StreamWriter(sHtaFilePath, False, System.Text.Encoding.UTF8)
        f.WriteLine("<html><title>" & sTitle & "</title><head><HTA:APPLICATION ID=oHTA SINGLEINSTANCE=""yes"" SCROLL=""yes""/></head>")
        f.WriteLine("<script language=""vbscript"">")
        f.WriteLine("Set fso = CreateObject(""Scripting.FileSystemObject"")")
        f.WriteLine("")

        f.WriteLine("Sub Send()")
        f.WriteLine(" Dim sFilePath: sFilePath = Replace(location.href,""file:///"","""")")
        f.WriteLine(" sFilePath = Replace(sFilePath,""/"",""\"")")
        f.WriteLine(" sFilePath = Replace(sFilePath,""%20"","" "")")
        f.WriteLine(" Set oXml = CreateObject(""Microsoft.XMLDOM"")")
        f.WriteLine(" Set oRoot = oXml.createElement(""root"")")
        f.WriteLine(" oXml.appendChild oRoot")
        f.WriteLine("sList = """"")
        f.WriteLine("For Each checkbox In document.getElementsByName(""chkBox"")")
        f.WriteLine("   If checkbox.checked Then")
        f.WriteLine("       If sList <> """" Then sList = sList & "",""")
        f.WriteLine("       sList = sList & checkbox.value")
        f.WriteLine("   End If")
        f.WriteLine("Next")
        f.WriteLine("AddXmlVal oXml, oRoot, ""List"", sList")
        f.WriteLine(" oXml.Save sFilePath & "".xml""")
        f.WriteLine(" self.Close()")
        f.WriteLine("End Sub")
        f.WriteLine("")

        f.WriteLine("Sub AddXmlVal(oXml, oRoot, sName, sVal)")
        f.WriteLine(" Set oNode = oXml.createElement(sName)")
        f.WriteLine(" oNode.Text = sVal")
        f.WriteLine(" oRoot.appendChild oNode")
        f.WriteLine("End Sub")
        f.WriteLine("")

        f.WriteLine("Sub SelectAll(b)")
        f.WriteLine("For Each checkbox In document.getElementsByName(""chkBox"")")
        f.WriteLine("   checkbox.checked = b")
        f.WriteLine("Next")
        f.WriteLine("End Sub")
        f.WriteLine("")

        f.WriteLine("</script>")
        f.WriteLine("<body>")
        f.WriteLine(sHtml)
        f.WriteLine("</body></html>")
        f.Close()
    End Sub

    Public Function CompareStrings(oldString As String, newString As String) As String
        Dim result As New System.Text.StringBuilder()
        Dim oldChars() As Char = oldString.ToCharArray()
        Dim newChars() As Char = newString.ToCharArray()
        Dim lcs As String = LongestCommonSubsequence(oldString, newString)
        Dim i As Integer = 0
        Dim j As Integer = 0
        Dim k As Integer = 0

        While i < oldChars.Length Or j < newChars.Length
            If k < lcs.Length AndAlso i < oldChars.Length AndAlso oldChars(i) = lcs(k) Then
                ' Character is in both strings
                result.Append(oldChars(i))
                i += 1
                j += 1
                k += 1
            ElseIf i < oldChars.Length Then
                ' Character is only in old string (deletion)
                If result.Length = 0 OrElse result(result.Length - 1) <> ">"c Then
                    result.Append("<span style='color: red;'>")
                End If
                result.Append(oldChars(i))
                i += 1
            Else
                ' Character is only in new string (addition)
                If result.Length = 0 OrElse result(result.Length - 1) <> ">"c Then
                    result.Append("<span style='color: green;'>")
                End If
                result.Append(newChars(j))
                j += 1
            End If

            ' Close span if necessary
            If (k < lcs.Length AndAlso ((i < oldChars.Length AndAlso oldChars(i) = lcs(k)) OrElse
                                    (j < newChars.Length AndAlso newChars(j) = lcs(k)))) AndAlso
           result.Length > 0 AndAlso result(result.Length - 1) <> ">"c Then
                result.Append("</span>")
            End If
        End While

        ' Close any open span
        If result.Length > 0 AndAlso result(result.Length - 1) <> ">"c Then
            result.Append("</span>")
        End If

        Return result.ToString()
    End Function

    Private Function LongestCommonSubsequence(s1 As String, s2 As String) As String
        Dim m As Integer = s1.Length
        Dim n As Integer = s2.Length
        Dim L(m, n) As Integer

        ' Build LCS matrix
        For i As Integer = 0 To m
            For j As Integer = 0 To n
                If i = 0 OrElse j = 0 Then
                    L(i, j) = 0
                ElseIf s1(i - 1) = s2(j - 1) Then
                    L(i, j) = L(i - 1, j - 1) + 1
                Else
                    L(i, j) = Math.Max(L(i - 1, j), L(i, j - 1))
                End If
            Next
        Next

        ' Reconstruct LCS
        Dim index As Integer = L(m, n)
        Dim lcs(index - 1) As Char

        Dim x As Integer = m
        Dim y As Integer = n
        While x > 0 AndAlso y > 0
            If s1(x - 1) = s2(y - 1) Then
                lcs(index - 1) = s1(x - 1)
                x -= 1
                y -= 1
                index -= 1
            ElseIf L(x - 1, y) > L(x, y - 1) Then
                x -= 1
            Else
                y -= 1
            End If
        End While

        Return New String(lcs)
    End Function

    Sub OpenHTAAndWait(htaFilePath As String)
        Dim process As New Process()

        process.StartInfo.FileName = "mshta.exe"
        process.StartInfo.Arguments = """" & htaFilePath & """"
        process.StartInfo.UseShellExecute = False

        process.Start()
        process.WaitForExit()
    End Sub

    Private Sub btnUpdateFileGrid_Click(sender As Object, e As EventArgs) Handles btnUpdateFileGrid.Click
        UpdateFileGrid()
    End Sub

End Class

 

Points of Interest

Creating a Text document out of PDF can be vary time consuming. I am planning to create another project that would do so by using OpenAI Vision API.  For now the best way is to upload your PDF file to Google Drive.  Google will automatically OCR it for you.  Next use AutoHotkey  convert the PDF to text so that each paragraph is a line of text.  For example, by using this AutoHotkey code below you can highlight a  paragraph in PDF file and press Windows Key.  AutoHotkey will append the text to your Text file.

C++
#Persistent
#SingleInstance Force
; Define the hotkey Ctrl + Shift + C
~LWin::
    Clipboard := ""
    Send, ^c
    ClipWait, 4
    selectedText := Clipboard
    StringReplace, selectedText, selectedText, `r`n, %A_Space%, All
    StringReplace, selectedText, selectedText, `n, %A_Space%, All
    StringReplace, selectedText, selectedText, `t, %A_Space%, All
    filePath := "C:\Audio\Test.txt"
    FileAppend, %selectedText%`r`n, %filePath%, UTF-8
return

History

Version 1 was created on July 26, 2024

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)