65.9K
CodeProject 正在变化。 阅读更多。
Home

音频书创建器

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2023年5月23日

CPOL

5分钟阅读

viewsIcon

4235

downloadIcon

414

此应用程序允许您使用 Eleven Labs API 创建有声书。

引言

此应用程序允许您使用 Eleven Labs API 创建有声书。 Eleven Labs API 每次请求仅允许 5,000 个字符。该应用程序将为每个段落生成 mp3 文件,然后将它们合并在一起。

如果您有一个 pdf 文件,则需要通过在 Word 中打开 pdf 文件并复制并粘贴到文本文件来生成文本文件。 在具有行号的文本编辑器(例如 Notepad2)中打开文本文件。 打开文件并删除不会被读取的文本,例如:目录、脚注、索引和参考文献。 每个段落必须在同一行上。

段落之间有 0.3 秒的停顿。 一个空行表示 1 秒的停顿。 两个空行表示新的章节。

一个空行表示 1 秒的停顿。 但是您可以使用“静音”功能自定义暂停时长。

如何创建有声读物

  • 首先从 Eleven Labs 获取 API 密钥 (https://beta.elevenlabs.io/pricing)
  • 选择 Eleven Labs “语音”。 可以创建和使用自定义语音。
  • “朗读”将基于任何文本生成 mp3 文件,并将文件放入与 EXE 相同的文件夹中的 Temp 文件夹中。
  • “何时突出显示文本”选项可帮助您在生成 mp3 文件之前进行文本文件更正和编辑。 例如,“以小写字符开头”和“以数字开头”选项将突出显示在 Word PDF 转换期间可能被破坏的段落。 “包含数字”可能有助于识别包含脚注编号的段落。
  • “保存文本文件”保存对文本文件的更改。“备份文本文件”选项创建一个备份,让您撤消对文本文件所做的更改。 备份文本文件将放置在与文本文件名相同名称的文件夹中,并加上“_backup”。
  • 1. 处理文本文件”将为文本文件中的每一行生成 MP3 文件。 这可能花费您大约 50 美元,具体取决于文件的大小。(当前价格为每 0.5 MB 99 美元。平均一本书大约 0.25 MB。)该文件将放置在与文本文件名相同名称的文件夹中。 每个文件将以文本文件中的行号命名,例如 0001.mp3。 这意味着在生成 MP3 文件后,您不应在文本文件中添加或删除行。
  • 在网格中选择一行,然后单击“播放”以播放 MP3 文件。 单击“停止”以停止 mp3 文件的播放。 在网格中选择一行,然后单击“删除”以删除 MP3 文件。
  • 您可以删除错误的 mp3 文件,然后再次单击“1. 处理文本文件”以重新生成已删除的 mp3 文件。
  • 您也可以在网格中选择一行,然后单击“重新生成”以重新创建 MP3 文件。 如果需要,此选项还会保存文本文件。
  • 按下按键时播放”选项允许您在选择一行文本后按下向下箭头键来收听整本书。
  • 对生成的 mp3 文件的质量感到满意后,单击“2. MP3 章节”以生成每个章节的 mp3 文件。 文本文件中的两个空行表示新的章节。 文件将放置在与文本文件名相同名称的文件夹中,并加上“-Chapters”。
  • 将MP3章节文件合并为一个mp3文件。该mp3文件将放置在与文本文件相同的文件夹中,名称相同,但扩展名为mp3。

将有声读物上传到YouTube

  • 首先选择将用于mp4文件生成的图像文件。
  • 3. 制作 MP4 文件”将为每个章节生成 mp4 文件。 此操作使用来自“-Chapters”文件夹的 mp3 文件。 此操作可能需要大约 8 小时。 然后可以将这些文件上传到 YouTube。 文件将放置在与文本文件名相同名称的文件夹中,并加上-Videos
  • 制作视频文件”将生成一个 mp4 文件。 然后可以将这些文件上传到 YouTube。 该文件将通过合并 MP4 章节文件(如果可用)来生成。 如果 MP4 章节文件不可用,则将使用单个 mp3 文件,但该操作可能需要大约 8 小时。 MP4 文件将放置在与文本文件相同的文件夹中,并具有相同的名称但带有 mp4 扩展名。
  • 根据章节mp3文件的时长生成YouTube索引。该索引可用于视频描述或评论部分。请注意,MP4文件可能会以不同的时长生成,因此可能需要调整索引。

这是我使用此应用程序创建的有声读物。

将有声读物上传到 Audible (acx.com)

  • Audible 需要 192 比特率的 MP3 文件。“制作 192 比特率文件”操作使用来自“-Chapters”文件夹的 mp3 文件。 mp3 192 比特率文件将放置在与文本文件名相同名称的文件夹中,并加上“-Chapters192”。
  • Audible (acx.com) 有时需要增加或减少 mp3 文件值。“更改音量”选项允许您执行此操作。 如果 ACX 仍然抱怨,请尝试使用 Audacity

Using the Code

代码使用 ffmpeg.exe 将 mp3 转换为 mp4 并更改 mp3 比特率。 这是主窗体的 VB.NET 代码

Imports System.Net
Imports System.IO

Public Class Form1

  'Dim API_KEY As String = "" 'https://beta.elevenlabs.io - Profile 
  'Dim sVoiceId As String = "21m00Tcm4TlvDq8ikWAM" 'Rachel
  Dim sVoiceId As String = "ErXwobaYiN019PkySvjV" 'Antoni
  Dim oVoices As New Hashtable
  Dim oAppSetting As New AppSetting()
  Dim bStop As Boolean = False

  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")

    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

    Dim sVoice As String = oAppSetting.GetValue("Voice", "Antoni")
    If sVoice <> "Antoni" And txtApiKey.Text <> "" Then

      Try
        SetVoices()
      Catch ex As Exception
        txtApiKey.Text = ""
      End Try

      SetVoiceSelect(sVoice)
    Else
      cbVoice.SelectedIndex = 0
    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 & _
              "This might cost you about $50 depending _
                            on the size of the 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(btn192Bitrate, "Audible requires MP3 file _
            with 192 bitrate. This operation uses mp3 files from _
            -Chapters folder." & vbCrLf & "The mp3 192 bitrate files will be _
       placed in the folder with the same name as the text file name _
            plus -Chapters192")

    ToolTip1.SetToolTip(btnChangeVolume, "Audible (acx.com) _
           sometimes requires the mp3 file values to be increased or _
           decreased. " & vbCrLf & "This option allows you to do that. _
          If ACX still complains try using Audacity.")

    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 Eleven Labs (https://beta.elevenlabs.io/pricing)"
    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 = "Eleven Labs Voice. A custom voice can be created."
    ToolTip1.SetToolTip(cbVoice, sTootip)
    ToolTip1.SetToolTip(btnReloadVoices, 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(urlApiKey, "Profile > API Key")

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

  End Sub

  Private Sub Form1_FormClosing(sender As Object, _
   e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
    oAppSetting.SetValue("Voice", cbVoice.Text)
    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.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 MsgBox("Are you sure you want to process the text file? _
                  This might cost you about $50.", vbYesNo) <> vbYes Then
      Exit Sub
    End If

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

    ProcessTextFile(0)
    UpdateFileGrid()

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

  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

    Dim sVoice As String = cbVoice.Text
    If oVoices.ContainsKey(sVoice) Then
      sVoiceId = oVoices(sVoice)
    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)

        If IO.File.Exists(sDestFilePath) = False Then
          If Trim(sLine) = "{{100ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\100ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{200ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\200ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{300ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\300ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{400ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\400ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{500ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\500ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{600ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\600ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{700ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\700ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{800ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\800ms.mp3", sDestFilePath)

          ElseIf Trim(sLine) = "{{900ms.mp3}}" Then
            IO.File.Copy(sMp3FolderePath & "\900ms.mp3", 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
              MsgBox("Could not generate file for line: " _
                                   & iRow & ", Text: " & sLine)
            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
      lbCount.Visible = False
      btnStop.Visible = False
      ProgressBar1.Value = 1
      ProgressBar1.Visible = False
    End If

  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"))

    Dim sVoice As String = cbVoice.Text
    If oVoices.ContainsKey(sVoice) Then
      sVoiceId = oVoices(sVoice)
    End If

    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

    If Trim(sText) = "" Then
      Return False
    End If

    If txtApiKey.Text = "" Then
      MsgBox("Set API Key")
      Return False
    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://docs.elevenlabs.io/api-reference/text-to-speech
    Dim apiEndpoint As String = _
       "https://api.elevenlabs.io/v1/text-to-speech/" & sVoiceId 'Rachel
    Dim request As HttpWebRequest = WebRequest.Create(apiEndpoint)
    request.Method = "POST"
    request.ContentType = "application/json"
    request.Accept = "audio/mpeg"
    request.Headers.Add("xi-api-key", txtApiKey.Text)

    Dim data As String = "{"
    data += " ""text"":""" & PadQuotes(sText) & ""","
    'data += " ""voice_settings"": {""stability"": 0,""similarity_boost"": 0}"
    data += " ""voice_settings"": {""stability"": 0.5,""similarity_boost"": 1}"
    data += "}"

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

    Dim response As HttpWebResponse = request.GetResponse()

    If response.StatusCode = 200 Then
      Dim oFileStream As FileStream = IO.File.Create(sFilePath)
      response.GetResponseStream().CopyTo(oFileStream)
      oFileStream.Close()
      Return True
    Else
      Return False
    End If

  End Function

  Private Sub btnReloadVoices_Click(sender As Object, e As EventArgs) _
                                     Handles btnReloadVoices.Click
    SetVoices()
  End Sub

  Private Sub SetVoices()

    If txtApiKey.Text = "" Then
      MsgBox("Set API Key")
      Exit Sub
    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

    Dim apiEndpoint As String = "https://api.elevenlabs.io/v1/voices"
    Dim request As HttpWebRequest = WebRequest.Create(apiEndpoint)
    request.Method = "GET"
    request.ContentType = "application/json"
    request.Headers.Add("xi-api-key", txtApiKey.Text)

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

    Dim sVoice As String = cbVoice.Text
    cbVoice.Items.Clear()

    'Dim sIds As String = ""
    Dim oSortedList As SortedList = New SortedList()
    Dim oJavaScriptSerializer As _
           New System.Web.Script.Serialization.JavaScriptSerializer
    Dim oJson As Hashtable = oJavaScriptSerializer.Deserialize(Of Hashtable)(sJson)
    Dim oList As Object() = oJson("voices")
    For i As Integer = 0 To oList.Length - 1
      Dim sId As String = oList(i)("voice_id")
      Dim sName As String = oList(i)("name")
      oSortedList.Add(sName, sName)
      oVoices(sName) = sId
    Next

    For Each oItem As DictionaryEntry In oSortedList
      cbVoice.Items.Add(oItem.Key)
    Next

    SetVoiceSelect(sVoice)

  End Sub

  Private Sub SetVoiceSelect(sVoice As String)
    For i As Integer = 0 To cbVoice.Items.Count - 1
      If cbVoice.Items(i) = sVoice Then
        cbVoice.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"
    OpenFileDialog1.ShowDialog()

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

  End Sub

  Private Sub UpdateFileGrid()
    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 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()

    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 Sub DataGridColor()

    Dim sHighlight As String = selHighlight.Text

    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 Len(sText) > 5000 Then
          'https://beta.elevenlabs.io/faq
            'https://help.elevenlabs.io/hc/en-us/articles/13298164480913-What-s-the-maximum-amount-of-text-I-can-generate-
          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
          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")))

    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 oMP3Info As New Monotic.Multimedia.MP3.MP3Info(sSrcFilePath)
          Dim iLength As Integer = oMP3Info.Length

          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

      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 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

    Dim sFileName As String = Path.GetFileNameWithoutExtension(sFilePath)
    Dim iProcessRow As Integer = sFileName
    ProcessTextFile(iProcessRow)
    UpdateFileGrid()
    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

      '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 = "Beginning"
    Dim sLine As String = oStreamReader.ReadLine()
    Do Until sLine Is Nothing
      iRow += 1

      If iBlankCount = 2 AndAlso Trim(sLine) <> "" Then
        sChapterName = PadFileName(Trim(sLine))
        iChapterCount += 1
      End If

      Dim sChapterName2 As String = Microsoft.VisualBasic.Right_
                             ("00" & iChapterCount, 2) & " " & sChapterName
      Dim sChapterFolderPath As String = _
                             Path.Combine(sDstFolderPath, sChapterName2)
      If System.IO.Directory.Exists(sChapterFolderPath) = False Then
        System.IO.Directory.CreateDirectory(sChapterFolderPath)
      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

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

  End Sub

  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

  Private Sub btn192Bitrate_Click(sender As Object, e As EventArgs) _
                                   Handles btn192Bitrate.Click

    Dim sFilePath As String = txtSrcFile.Text
    If sFilePath = "" Then
      MsgBox("Text 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")
      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

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

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

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

    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 sInputFileName As String = Path.GetFileName(sInputFilePath)
      Dim sOutputFilePath As String = Path.Combine(s192FolderPath, sInputFileName)

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

      Dim sArguments As String = "-i """ & sInputFilePath & """ -af ""volume=" _
               & sVolume & "dB"" -b:a ""192k"" """ & sOutputFilePath & """"
      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("mp3 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("mp3 file was created but is blank: " _
                   & sOutputFilePath & ". Command text is copied to Clipboard.")
        End If
      End If
    Next

    SetFileTags(s192FolderPath, sFileName)
    btn192Bitrate.Enabled = True
    ProgressBar1.Visible = False
    MsgBox("Done")

  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

  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

    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 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

      If IO.File.Exists(sOutputFilePath) = False 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, 100)
            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, 100)
          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)

    ' 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
      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(Color.Black)

    ' Create a font for the text. Start with a large size.
    Dim fontSize As Single = 72
    Dim font As New Font("Arial", fontSize)

    ' Calculate the size of the text
    Dim textSize As SizeF = graphics.MeasureString(text, font)

    ' If the text is too wide, decrease the font size 
       ' until it fits the image's width
    While textSize.Width > img.Width
      fontSize -= 1
      font = New Font("Arial", fontSize)
      textSize = graphics.MeasureString(text, font)
    End While

    If bottomMargin + 100 > img.Height Then
      bottomMargin = 0
    End If

    ' Define where the text will be placed 
       ' (bottom and center of the image, accounting for margin)
    Dim rect As New RectangleF((img.Width - textSize.Width) / 2, _
       img.Height - textSize.Height - bottomMargin, textSize.Width, textSize.Height)

    ' Draw the string onto the image
    graphics.DrawString(text, font, brush, rect)

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

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

  Sub MergeMp4Files(ByVal sVideoFolderPath As String, _
                     ByVal sOutputFilePath As String, ByVal sFfmpegFile As String)

    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)

    Dim sTextFilePath As String = Path.Combine(sTempFolderPath, "Files.txt")
    Dim sw As New StreamWriter(sTextFilePath, False)

    For Each sPath As String In IO.Directory.GetFiles(sVideoFolderPath)
      If Path.GetExtension(sPath) = ".mp4" Then
        Dim sName As String = Trim(Microsoft.VisualBasic.Left_
                   (Path.GetFileNameWithoutExtension(sPath), 3))
        Dim sFName As String = sName & ".mp4"
        Dim sToPath As String = Path.Combine(sTempFolderPath, sFName)
        File.Copy(sPath, sToPath)
        sw.WriteLine("file '" & sFName & "'")
      End If
    Next

    sw.Close()

    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 = "-f concat -i """ & sTextFilePath & """ _
                              -c copy """ & sOutputFilePath & """"
    oProcess.StartInfo = startInfo
    oProcess.Start()
    oProcess.WaitForExit()

    System.Threading.Thread.Sleep(1000)

    Try
      EmptyFolder(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)
      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
      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 btnChangeVolume_Click(sender As Object, e As EventArgs) _
                                     Handles btnChangeVolume.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 s192FolderPath As String = Path.Combine(sFolderPath, _
                                      sFileName & "-Chapters192")
    If System.IO.Directory.Exists(s192FolderPath) = False Then
      MsgBox("Chapters192 folder does not exist: " & s192FolderPath)
      Exit Sub
    End If

    Dim oChangeVolume As New frmChangeVolume
    oChangeVolume.s192FolderPath = s192FolderPath
    oChangeVolume.ShowDialog()
  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://beta.elevenlabs.io"))
  End Sub
End Class

历史

  • 2023 年 5 月 23 日:创建版本 1
© . All rights reserved.