Skip to content

Aeneas/SAB

Dido

Dido is a replacement for aeneas written in Go that doesn't suffer from all the idiosyncrasies and dependencies of the python version.

Drop-in replacement of aeneas doesn't work reliably, so we have to do a 2-part workflow for languages like Thai:

  • Create the app in SAB (Ubuntu) and synchronize then just cancel the job (cause it doesn't support Thai).
  • Copy the resultant phrase files to your OneDrive audio folder into a subfolder called timings.
  • In Windows, cd to the audio dir, and run the Powershell script below:
    powershell -ExecutionPolicy Bypass -File .\make_batch.ps1.
  • Run dido: "H:\SGC_git\dido\dido.exe" --batch "batch.json"
Powershell script: make_batch.ps1
param(
    [string]$AudioFolder = ".",
    [string]$TextSubfolder = "timings",
    [string]$OutputSubfolder = "timings",
    [string]$BatchFilename = "batch.json",

    [string]$Language = "tha",
    [string]$TextType = "parsed",
    [string]$OutputFormat = "tsv",

    [string]$BoundaryAlgorithm = "percent",
    [int]$BoundaryPercent = 50,

    # For your files:
    # Audio: "01 บทที่1 ..." -> use First
    # Text:  "C01-01-B001-15-aeneas.txt" -> use Last
    [ValidateSet("First", "Last")]
    [string]$AudioNumberPosition = "First",

    [ValidateSet("First", "Last")]
    [string]$TextNumberPosition = "Last"
)

$ErrorActionPreference = "Stop"

function Get-SequenceNumber {
    param(
        [string]$FileName,
        [string]$NumberPosition
    )

    $matches = [regex]::Matches($FileName, '\d+')

    if ($matches.Count -eq 0) {
        return $null
    }

    if ($NumberPosition -eq "First") {
        return [int]$matches[0].Value
    }

    return [int]$matches[$matches.Count - 1].Value
}

function Get-RelativePathForJson {
    param(
        [string]$BasePath,
        [string]$FullPath,
        [switch]$MustExist
    )

    $baseResolved = (Resolve-Path -LiteralPath $BasePath).Path

    if ($MustExist) {
        $fullResolved = (Resolve-Path -LiteralPath $FullPath).Path
    }
    else {
        $fullResolved = [System.IO.Path]::GetFullPath($FullPath)
    }

    $baseUri = [System.Uri]($baseResolved.TrimEnd('\') + '\')
    $fullUri = [System.Uri]$fullResolved

    $relative = $baseUri.MakeRelativeUri($fullUri).ToString()

    # Forward slashes are safe in Windows JSON paths and avoid escaping backslashes.
    return [System.Uri]::UnescapeDataString($relative)
}

$AudioFolder = (Resolve-Path -LiteralPath $AudioFolder).Path
$TextFolder = Join-Path $AudioFolder $TextSubfolder
$OutputFolder = Join-Path $AudioFolder $OutputSubfolder
$BatchPath = Join-Path $AudioFolder $BatchFilename

if (!(Test-Path -LiteralPath $TextFolder)) {
    throw "Could not find text folder: $TextFolder"
}

if (!(Test-Path -LiteralPath $OutputFolder)) {
    New-Item -ItemType Directory -Path $OutputFolder | Out-Null
}

$parameters = "task_language=$Language|is_text_type=$TextType|os_task_file_format=$OutputFormat|task_adjust_boundary_algorithm=$BoundaryAlgorithm|task_adjust_boundary_percent_value=$BoundaryPercent"

# Index timing text files by sequence number.
$textFilesByNumber = @{}

Get-ChildItem -LiteralPath $TextFolder -Filter "*.txt" -File | ForEach-Object {
    $num = Get-SequenceNumber -FileName $_.Name -NumberPosition $TextNumberPosition

    if ($null -eq $num) {
        Write-Warning "Ignoring text file with no sequence number: $($_.Name)"
        return
    }

    if ($textFilesByNumber.ContainsKey($num)) {
        Write-Warning "Duplicate text file number $num found. Keeping first: $($textFilesByNumber[$num].Name); ignoring: $($_.Name)"
        return
    }

    $textFilesByNumber[$num] = $_
}

$mp3Files = Get-ChildItem -LiteralPath $AudioFolder -Filter "*.mp3" -File | Sort-Object Name

if ($mp3Files.Count -eq 0) {
    throw "No MP3 files found in $AudioFolder"
}

$audioFilesByNumber = @{}

foreach ($mp3 in $mp3Files) {
    $num = Get-SequenceNumber -FileName $mp3.Name -NumberPosition $AudioNumberPosition

    if ($null -eq $num) {
        Write-Warning "Skipping audio file with no sequence number: $($mp3.Name)"
        continue
    }

    if ($audioFilesByNumber.ContainsKey($num)) {
        Write-Warning "Duplicate audio file number $num found. Keeping first: $($audioFilesByNumber[$num].Name); ignoring: $($mp3.Name)"
        continue
    }

    $audioFilesByNumber[$num] = $mp3
}

$tasks = @()

foreach ($num in ($audioFilesByNumber.Keys | Sort-Object)) {
    $mp3 = $audioFilesByNumber[$num]

    if (!$textFilesByNumber.ContainsKey($num)) {
        Write-Warning "Skipping $($mp3.Name): no matching text file found for sequence number $num"
        continue
    }

    $textFile = $textFilesByNumber[$num]

    # $audioStem = [System.IO.Path]::GetFileNameWithoutExtension($mp3.Name)
    # $outputFile = Join-Path $OutputFolder "$audioStem-timing.txt"

    $textStem = [System.IO.Path]::GetFileNameWithoutExtension($textFile.Name)

    # If the phrase file is named like C01-01-B001-01-aeneas.txt,
    # the timing file becomes C01-01-B001-01-timing.tsv.
    if ($textStem.EndsWith("-aeneas")) {
        $textStem = $textStem.Substring(0, $textStem.Length - "-aeneas".Length)
    }

    $outputFile = Join-Path $OutputFolder "$textStem-timing.txt"

    $description = ("B001 {0:D2}" -f $num)

    $task = [ordered]@{
        description    = $description
        audioFilename  = Get-RelativePathForJson -BasePath $AudioFolder -FullPath $mp3.FullName -MustExist
        phraseFilename = Get-RelativePathForJson -BasePath $AudioFolder -FullPath $textFile.FullName -MustExist
        outputFilename = Get-RelativePathForJson -BasePath $AudioFolder -FullPath $outputFile
        parameters     = $parameters
    }

    $tasks += [pscustomobject]$task

    Write-Host "Matched $description"
    Write-Host "  Audio: $($mp3.Name)"
    Write-Host "  Text:  $($textFile.Name)"
    Write-Host "  Out:   $([System.IO.Path]::GetFileName($outputFile))"
}

if ($tasks.Count -eq 0) {
    throw "No matching audio/text pairs were found."
}

$json = $tasks | ConvertTo-Json -Depth 10

# UTF-8 without BOM, safer for Thai filenames.
[System.IO.File]::WriteAllText($BatchPath, $json, [System.Text.UTF8Encoding]::new($false))

Write-Host ""
Write-Host "Created batch file:"
Write-Host "  $BatchPath"
Write-Host ""
Write-Host "Matched task count: $($tasks.Count)"
Write-Host ""

Espeak-ng

In order to get the latest version of espeak-ng (that supports languages like Thai) for Windows, visit the Github repo's Actions page and look for auto-compiled Artifacts. Or you can build it yourself on Linux.

Aeneas (OUTDATED)

  • Install Ubuntu 22.04 on WSL
  • Install aeneas dependencies
  • Install aeneas itself

Note

You must use an older version of python because of a deprecated dependency (numpy misc_util) in newer versions.

sudo apt update && sudo apt upgrade -y
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install espeak espeak-data libespeak1 libespeak-dev ffmpeg -y
sudo apt install python3.10 python3.10-venv libpython3.10-dev build-essential gcc libpq-dev -y

python3.10 -m venv aeneasenv2
source aeneasenv2/bin/activate
pip install numpy
pip install aeneas

Info

It looks like someone has forked aeneas and updated it to work with newer versions of numpy/python: https://github.com/naglis/aeneas

Scripture App Builder

Install scripture app builder from https://packages.sil.org/ :

(wget -O- https://packages.sil.org/keys/pso-keyring-2016.gpg | sudo tee /etc/apt/trusted.gpg.d/pso-keyring-2016.gpg)&>/dev/null
(. /etc/os-release && sudo tee /etc/apt/sources.list.d/packages-sil-org.list>/dev/null <<< "deb http://packages.sil.org/$ID $VERSION_CODENAME main")
sudo apt update
sudo apt install scripture-app-builder

Install WSL2 from the Windows Store to enable GUI programs, then run scripture-app-builder & to open Scripture App Builder.