; ENVIRONMENT ;------------------------------------------------ #NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases. ;#Warn ; Enable warnings to assist with detecting common errors. ;DetectHiddenWindows, On #SingleInstance, Force SendMode Input ; Recommended for new scripts due to its superior speed and reliability. SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory. ;SetKeyDelay, 500 CoordMode, ToolTip, Screen CoordMode, Mouse, Screen ;#NoTrayIcon ; Notes/Extra Info/#Includes ;------------------------------------------------ ; VARIABLES ;------------------------------------------------ EnvGet, ProcessorCount, NUMBER_OF_PROCESSORS ; FONTS HeaderFontSize := 10 NormalFontSize := 10 Margin := 10 gui, Margin, %Margin%, 10 ColumnOneXPos := 10 GUIHeight = 400 ColumnHeaderButtonHeight := 30 ButtonHeight := 20 ExecuteButtonYPos := GUIHeight - ButtonHeight ColumnOneHeaderWidth := 250 ColumnTwoHeaderWidth := 300 ColumnThreeHeaderWidth := 150 ; ColumnOneHeaderWidth ColumnTwoVerticalLineXPos := ColumnOneHeaderWidth + ColumnOneXPos + 10 ColumnTwoXPos := ColumnTwoVerticalLineXPos + Margin Column_Three_Vertical_line_x_Pos := ColumnTwoXPos + ColumnTwoHeaderWidth + Margin ColumnThreeXPos := Column_Three_Vertical_line_x_Pos + Margin ColumnTwoSubCheckboxesXPos := ColumnTwoXPos + 20 ColumnOneWidth := ColumnTwoXPos - ColumnOneXPos - margin ; MAIN SCRIPT ;------------------------------------------------ ; Column One ;------------------------------------------------ Gui, Font, s%HeaderFontSize% Gui, Font, Bold Gui, Add, Button, x%ColumnOneXPos% y0 w%ColumnOneHeaderWidth% h%ColumnHeaderButtonHeight%, FILE SELECTION Gui, font, normal ; Gui, Add, Text, w%ColumnOneHeaderWidth% center, Select Files to Transcribe ; Gui, font, normal Gui, Add, Edit, vFilesFilepath gUpdateGUI w%ColumnOneHeaderWidth% Gui, Add, Button, y+1 gSelectFile w%ColumnOneHeaderWidth%, Select Files to Transcribe Gui, Add, Checkbox, gUpdateGUI vTranscribeDirectory, Transcribe all files in this folder Gui, Font, s%HeaderFontSize% Gui, Font, Bold Gui, Add, Button, x%ColumnOneXPos% y+50 w%ColumnOneHeaderWidth% h%ColumnHeaderButtonHeight%, TRANSCRIPTION SETTINGS Gui, font, normal Gui, Add, Edit, gUpdateGUI w%ColumnOneHeaderWidth%, Gui, Add, UpDown, vCPUThreads Range1-20, %CPUThreads% Gui, Add, Text, x%margin% y+%margin% w%ColumnOneHeaderWidth% center, Number of CPU Threads If Using CPU (PC MAX: %ProcessorCount%) ; `nTotal Processors on PC: %ProcessorCount% gui, add, text, y+20 w%ColumnOneWidth% 0x10 ;Horizontal Line > Etched Gray Gui, Font, s%HeaderFontSize% Gui, Font, Bold Gui, Add, Button, x%ColumnOneXPos% w%ColumnOneHeaderWidth% h%ColumnHeaderButtonHeight%, MODEL Gui, Font, Normal ; Gui, Add, Checkbox, vCheckboxColOne, Checkbox Gui, Add, Checkbox, vBaseM checked%BaseM%, Base Gui, Add, Checkbox, vSmallM checked%SmallM%, Small Gui, Add, Checkbox, vMediumM checked%MediumM%, Medium Gui, Add, Checkbox, vLargeM checked%LargeM% ,Large Gui, Add, Checkbox, vAllMQualities, ALL (One by One) /* Gui, Font, Bold Gui, Add, Button, x%ColumnOneXPos% w%ColumnOneHeaderWidth% h%ColumnHeaderButtonHeight%, LENGTH Gui, font, normal Gui, Add, Checkbox, gUpdateGUI vShortPodcasts checked%ShortPodcastsCheckStatus%, Short ( < 30 Mins) Gui, Add, Checkbox, vMediumPodcasts gUpdateGUI checked%MediumPodcastsCheckStatus%, Medium (30-60 Mins) Gui, Add, Checkbox, vLongPodcasts gUpdateGUI checked%LongPodcastsCheckStatus%, Long ( > 60 Mins) Gui, Add, Checkbox, vAnyLengthMediaFile gUpdateGUI checked%AnyLengthMediaFileCheckStatus%, ANY Length */ ; Column Two ;------------------------------------------------ Gui, Font, s%HeaderFontSize% Gui, Font, Bold gui, add, text, x%ColumnTwoVerticalLineXPos% y0 h%GUIHeight% 0x11 ;Vertical Line > Etched Gray Gui, Add, Button, x%ColumnTwoXPos% y0 w%ColumnTwoHeaderWidth% h%ColumnHeaderButtonHeight%, Settings Gui, Font, Normal Gui, Add, Checkbox, vKeepCMD, Keep CMD Open After Model Completion Gui, Add, Checkbox, vShowTooltip checked%ShowTooltip%, Show Tooltip of Progress at Top of Screen Gui, Add, Checkbox, vNTFYPost checked%NTFYURLCheckStatus%, NTFY After Each Transcription Gui, Add, Checkbox, vNTFYUpload checked%NTFYUploadCheckStatus%, Upload Transcript to NTFY Gui, Add, Checkbox, vContinueAfterCMDClose checked%ContinueAfterCMDCloseCheckStatus%, Start After Current Running Transcription Ends ; Column Three ;------------------------------------------------ Gui, Font, s%HeaderFontSize% Gui, Font, Bold gui, add, text, x%Column_Three_Vertical_line_x_Pos% y0 h%GUIHeight% 0x11 ;Vertical Line > Etched Gray Gui, Add, Button, x%ColumnThreeXPos% y0 w%ColumnThreeHeaderWidth% h%ColumnHeaderButtonHeight%, COLUMNTHREE Gui, Font, Normal Gui, Add, Checkbox, vCheckboxColThree, Checkbox Gui, Font, Bold ; Gui, Add, Button, yp+70 w%ColumnThreeHeaderWidth% gUpgradeChocolatey, Upgrade Chocolatey ; Gui, Add, Button, yp+100 w%ColumnThreeHeaderWidth% gUncheckAll, Uncheck All Gui, Add, Button, w%ColumnThreeHeaderWidth% gSubmit h%ButtonHeight% y%ExecuteButtonYPos% x%ColumnThreeXPos%, Execute ; Gui, Add, Text, x+5, Debloat ; Gui, Show, ; w1230 ; , w600 h200 gui, Show,,%GUI_Title% Return ; GoSubs ;------------------------------------------------ UpdateGUI: gui, Submit, NoHide Return SelectFile: if(SelectedDirectoryPath){ Msgbox, Your already have a directory selected. `nThis script can only handle one or the other at the moment. return } FileSelectFile, SelectedFiles, M3 ; M3 = Multiselect existing files. if (SelectedFiles = "") return GUIFilesText := StrReplace(SelectedFiles, "`n", "|") GuiControl, Text,FilesFilepath, %GUIFilesText% return SelectDirectory: if(SelectedFiles){ Msgbox, Your already have individual files selected. `nThis script can only handle one or the other at the moment. return } ; have user select a file within the directory they want transcribed. ; Will later get the directory name from this file FileSelectFile, SelectedDirectoryPath if (SelectedDirectoryPath = "") return GuiControl,Text,SelectedDirectoryPath, %SelectedDirectoryPath% return GuiClose: ExitApp ReloadScript: Reload ExitApp Submit: gui, Submit, NoHide if(NTFY){ IniRead, NTFYURL, Settings.ini, %ScriptName%, NTFYURL, %A_space% ; Msgbox % "NTFYURL: " NTFYURL if(!NTFYURL){ InputBox, NTFYURL, Input NTFY URL, Please input NTFY url for notifications.`nGenerate a URL at: https://ntfy.sh/app if(ErrorLevel){ NTFY := 0 } else, { ; save url to settings.ini IniWrite, %NTFYURL%, Settings.ini, %ScriptName%, NTFYURL } } } IniWrite, %CPUThreads%, Settings.ini, %ScriptName%, CPUThreads IniWrite, %ShowTooltip%, Settings.ini, %ScriptName%, ShowTooltip IniWrite, %MinutesToPauseBetweenTranscriptions%, Settings.ini, %ScriptName%, MinutesToPauseBetweenTranscriptions ; IniWrite, %ContinueAfterCMDClose%, Settings.ini, %A_ScriptName%, ContinueAfterCMDClose if(SelectedDirectoryPath){ IniWrite, %SelectedDirectoryPath%, Settings.ini, %ScriptName%, SelectedDirectoryPath } else, { IniWrite, %A_space%, Settings.ini, %ScriptName%, SelectedDirectoryPath } if(!SelectedFiles AND !SelectedDirectoryPath){ msgbox, Please select individual files or a directory that you want Transcribed. return } Status := RunCMD("ffmpeg") ; Msgbox % "Status: " Status if(!InStr(Status, "ffmpeg version")){ MsgBox, Error:, FFMPEG was not found in System PATH.`nPlease install it and add it to System PATH to automatically convert files to the Whisper.cpp required 16-bit WAV file return } Message = Creating Required Variables ShowTooltipText(Message) ; if files selected, add them all to array if(SelectedFiles){ SelectedAudioFilesArray := [] ; Create array ; loop through all selected files and add them to the array Loop, parse, SelectedFiles, `n { if (A_Index = 1){ AudioFilesDIR = %A_LoopField% ; MsgBox, The selected files are all contained in %A_LoopField%. } else { AudioFileFP = %AudioFilesDIR%\%A_LoopField% SelectedAudioFilesArray.Push(AudioFileFP) ; Append an item to the array ; SelectedAudioFilesVAR .= AudioFileFP . "," } } } ; if directory selected, loop through all files and add them to the array if(SelectedDirectoryPath){ SplitPath, SelectedDirectoryPath, OutFileName, AudioFilesDIR, OutExtension, OutNameNoExt, OutDrive ; msgbox, adding files in directory to var SelectedAudioFilesArray := [] ; Create array Text = Creating Array of Whitelisted Files (%WhitelistedMediaFilesExtensions%) `nin Directory: %AudioFilesDIR% ShowTooltipText(Text) PrePendToErrorLogText(text) Loop, files, %AudioFilesDIR%\*.*, F { SplitPath, A_LoopFileFullPath, FileNameWExt, FileDir, FileExt, FileNameNoExt, if(InStr(WhitelistedMediaFilesExtensions, FileExt)){ SelectedAudioFilesArray.Push(A_LoopFileFullPath) } } } ; If directory selected, add all files to array ; Msgbox % "AudioFilesDIR: " AudioFilesDIR ; KeepCMD := 0 OverwritePreviousTranscriptions := 0 if(KeepCMD){ KeepCMD := "K" } else, { KeepCMD := "C" } ; clear variable from GUI TranscriptionModelsSelected := ; push to array each quality selected if(TinyM Or AllMQualities){ TranscriptionModels.Push("Tiny") ; Append an item to the array TranscriptionModelPaths.Push(tinyM_FP) ; Append an item to the array TranscriptionModelsSelected .= "Tiny|" } if(SmallM Or AllMQualities){ TranscriptionModels.Push("Small") ; Append an item to the array TranscriptionModelPaths.Push(smallM_FP) ; Append an item to the array TranscriptionModelsSelected .= "Small|" } if(BaseM Or AllMQualities){ TranscriptionModels.Push("Base") ; Append an item to the array TranscriptionModelPaths.Push(baseM_FP) ; Append an item to the array TranscriptionModelsSelected .= "Base|" } if(MediumM Or AllMQualities){ TranscriptionModels.Push("Medium") ; Append an item to the array TranscriptionModelPaths.Push(mediumM_FP) ; Append an item to the array TranscriptionModelsSelected .= "Medium|" } if(LargeM Or AllMQualities){ TranscriptionModels.Push("Large") ; Append an item to the array TranscriptionModelPaths.Push(largeM_FP) ; Append an item to the array TranscriptionModelsSelected .= "Large|" } if(TranscriptionModelsSelected = ""){ ToolTip Text = Please Select at Least ONE Model to Transcribe With msgbox, %Text% return } ; save selected models to ini files IniWrite, %TranscriptionModelsSelected%, Settings.ini, %ScriptName%, TranscriptionModelsSelected TranscriptionLengthsSelected := (ShortPodcasts)?(TranscriptionLengthsSelected .= "Short|"):("") (MediumPodcasts)?(TranscriptionLengthsSelected .= "Medium|"):("") (LongPodcasts)?(TranscriptionLengthsSelected .= "Long|"):("") (AnyLengthMediaFile)?(TranscriptionLengthsSelected .= "AnyLengthMediaFile|"):("") if(TranscriptionLengthsSelected = ""){ ToolTip Text = Please Select the Audio Length you'd like to Transcribe msgbox, %Text% return } IniWrite, %TranscriptionLengthsSelected%, Settings.ini, %ScriptName%, TranscriptionLengthsSelected ; msgbox text = Creating Required Directories ShowTooltipText(Text) FileCreateDir, %AudioFilesDIR%\Transcriptions FileCreateDir, %AudioFilesDIR%\WAVFiles text = Starting Transcription ShowTooltipText(Text) PrePendToErrorLogText(text) ; loop through the array and transcribe using options selected ; Msgbox % TranscriptionModels.Length() ; Display total number of items in the array ; ArrayLenght := TranscriptionModels.Length() ; Save total number of items in the array if(ContinueAfterCMDClose and WinExist("Podcast-Transcription-In-Progress")){ ; msgbox, cmd found. Text = Waiting for Current Running Transcription to Finish in CMD ShowTooltipText(Text) PrePendToErrorLogText(text) WinWaitClose, ahk_class ConsoleWindowClass ;, WinText, Seconds [, ExcludeTitle, ExcludeText] ; msgbox, cmd closed } ; loop for each selected audio file loop % SelectedAudioFilesArray.Length() { Filepath := SelectedAudioFilesArray[A_Index] OriginalFilepath := SelectedAudioFilesArray[A_Index] SplitPath, Filepath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive ; Convert the file to 16-bit WAV file as required by whisper.cpp WavFilepath = %AudioFilesDIR%\WAVFiles\%OutNameNoExt%.wav ; loop through each selected transcription model Loop % TranscriptionModels.Length() { ModelName := TranscriptionModels[A_Index] ModelPath := TranscriptionModelPaths[A_Index] ; create filepaths where outputs will be saved to OutputPath = %AudioFilesDIR%\Transcriptions\%OutNameNoExt%_%ModelName% OutputModelTxtFile = %AudioFilesDIR%\Transcriptions\%OutNameNoExt%_%ModelName%.txt OutputTxtFP = %OutputPath%.txt LogFileFP = %OutputPath%.log Text = Checking for Previous Transcription ShowTooltipText(Text) ; If .txt Transcription file exists, move on to next model if(FileExist(OutputModelTxtFile)){ Message :="Skipping " . OutFileName . " - already transcribed with " . ModelName . " Model" PrePendToErrorLogText(Message) Continue } ; otherwise check if log file exists from a previous run, or current run on different node (if using shared folder) if(FileExist(LogFileFP)){ FileGetTime, LogFileFPModificationTime, %LogFileFP%, M ; ; EnvSub, Var, Value [, TimeUnits] EnvSub, LogFileFPModificationTime, A_Now, S ; get hours SINCE last modified ; Check if log file was last added to more than 24 hours ago ; No podcast transcription should take more than that, and it means that a node started, but never finished it LogFileFPModificationTime := (LogFileFPModificationTime * -1 ) ; Msgbox % "LogFileFPModificationTime: " LogFileFPModificationTime if((LogFileFPModificationTime < 86400)){ text = Skipping %OutFileName% with Model:%ModelName% - Currently being Transcribed by a different node. PrePendToErrorLogText(text) ShowTooltipText(Text) ; sleep, 1000 Continue } } ; get information about audio file Obj := Filexpro(Filepath,, , "Length" , "Size" ) AudioLength := obj["Length"] AudioLengthArray := StrSplit(AudioLength,":") TotalTimeInSeconds := ((AudioLengthArray[1] * 60 ) * 60) + (AudioLengthArray[2] * 60) + AudioLengthArray[3] if(AnyLengthMediaFile){ ; continue onwards } else, { ; otherwise check length and skip accordingly if(ShortPodcasts){ if(TotalTimeInSeconds > 1860){ ; greater than 31 minutes Text = Podcast is longer than 30 minutes. Skipping ShowTooltipText(Text) PrePendToErrorLogText(text) ; msgbox, skipping medium/Long podcast Continue } } if(MediumPodcasts){ ; if less than 31 minutes or greater than 61 minutes if(TotalTimeInSeconds < 1860 OR TotalTimeInSeconds > 3660){ Text = Podcast is either shorter than 30 minutes or longer than 60 minutes. Skipping ShowTooltipText(Text) PrePendToErrorLogText(text) Continue } } if(LongPodcasts){ if(TotalTimeInSeconds < 3660){ Text = Podcast is shorter than 60 minutes Skipping ShowTooltipText(Text) PrePendToErrorLogText(text) ; msgbox, skipping short/medium podcast Continue } } } if(FileExist(WavFilepath)){ ; re-use previously converted wav file if it exists Filepath := WavFilepath }else, { text = Converting File: %OutFileName% To 16-bit WAV file ShowTooltipText(Text) PrePendToErrorLogText(text) runwait, %ComSpec% /%KeepCMD% ffmpeg -i "%Filepath%" -ar 16000 -ac 1 -c:a pcm_s16le "%WavFilepath%" } ; msgbox, checking filepath if(!FileExist(WavFilepath)){ Message = WAV file not found at:`n%WavFilepath%`nConversion failed for some reason.`nPlease select the "Keep CMD Open" checkbox and run again to see the error.`nClick OK to exit. PrePendToErrorLogText(text) Continue } FormatTime, TodayDate , YYYYMMDDHH24MISS,hh:mm text = Transcription in Progress`nModel: %ModelName% CPU Threads: %CPUThreads% Length: %AudioLength%`nFile: %OutNameNoExt%`nStart Time: %TodayDate% LogToTextFile(text, LogFileFP) PrePendToErrorLogText(text) ShowTooltipText(Text) ; msgbox UStartTime := A_TickCount ; start time runwait, %ComSpec% /%KeepCMD% title Podcast-Transcription-In-Progress & %A_scriptdir%\Models\main.exe -m %ModelPath% -f "%WavFilepath%" -t %CPUThreads% -otxt -ovtt -osrt -owts -ocsv -of "%OutputPath%" URunTime := round(((A_TickCount - UStartTime) / 1000), 2) ; end time Message = Time to Transcribe: %URunTime% seconds LogToTextFile(Message, LogFileFP) ; get time it took to transcribe in seconds TranscriptionLength := round(((A_TickCount - UStartTime) / 1000), 2) ; if more than 1 hour, convert time to hours:minutes for NTFY message (TranscriptionLength > 3600)?(NTFYTranscriptionTime := round((TranscriptionLength / 3600),2) . " Hours"):(NTFYTranscriptionTime := TranscriptionLength . " Seconds") SecondsToTranscribe1Sec := round((TranscriptionLength / TotalTimeInSeconds), 2) ; TotalTimeInSeconds text = Transcription of %WavFilepath% took a total of %TranscriptionLength% Seconds LogToTextFile(text, LogFileFP) PrePendToErrorLogText(text) if(NTFY){ Message = Transcription Complete `nNode: %A_ComputerName% `nFile: %OutNameNoExt% `nAudio Length: %AudioLength%`nModel: %ModelName%`nCPU Threads: %CPUThreads% `nTTTS: %NTFYTranscriptionTime%`nTTS 1 (s): %SecondsToTranscribe1Sec% Seconds SendMessagetoNTFY(Message, NTFYURL) } } ; / All transcriptions have been looped through ; take a short rest so CPU has time to cool down ; IF the last model tool longer than 10 seconds to complete (AKA not skipped over) if(MinutesToPauseBetweenTranscriptions AND TranscriptionLength > 1000){ if(ModelName = "Medium" OR ModelName="Large"){ text = Waiting for %MinutesToPauseBetweenTranscriptions% Minutes before moving on to next item ShowTooltipText(Text) PrePendToErrorLogText(text) MilisecondsToSleep := MinutesToPauseBetweenTranscriptions * 60000 sleep, %MilisecondsToSleep% } ; reset timer for next loop TranscriptionLength := } } ToolTip if(NTFY){ Message = Node: %A_ComputerName% Has Completed Transcribing ALL Selected Files! ;`nFile: %OutNameNoExt% `nModel: %ModelName%`nCPU Threads: %CPUThreads% `nLength: %AudioLength%`nTime: %TranscriptionLength% Seconds SendMessagetoNTFY(Message, NTFYURL) } msgbox, All Transcriptions have been completed. return ; Functions ;------------------------------------------------ LogToTextFile(Text, Filepath){ ; ErrorLoggingFile := Filepath FileAppend, %Text%`n`n, %Filepath% } ShowTooltipText(Text){ if(ShowTooltip){ TooltipFirstLine := StrSplit(Text, "`n") TooltipFirstLine := TooltipFirstLine[1] ToolTipLen := StrLen(TooltipFirstLine) TooltipXPos := A_ScreenWidth / 2 - ((ToolTipLen * 9) / 2) ; Msgbox % "TooltipXPos: " TooltipXPos ToolTip, %Text%, %TooltipXPos%, 0 } } PrePendToErrorLogText(Text){ ErrorLogText := Text "`n`n" . ErrorLogText GuiControl, Text,StatusLog, %ErrorLogText% } SendMessagetoNTFY(Message, URL){ command = powershell (Invoke-RestMethod -Method 'Post' -Uri %URL% -Body '%Message%' -UseBasicParsing) Status := RunCMD(Command) ; Msgbox % "Status: " Status } /* */ ; -------------------------------FileXPro Get File Attributes------------------------------- ;https://www.autohotkey.com/boards/viewtopic.php?t=59882 Filexpro( sFile := "", Kind := "", P* ) { ; v.90 By SKAN on D1CC @ goo.gl/jyXFo9 Local Static xDetails If ( sFile = "" ) { ; Deinit static variable xDetails := "" Return } fex := {}, _FileExt := "" Loop, Files, % RTrim(sfile,"\*/."), DF { If not FileExist( sFile:=A_LoopFileLongPath ) { Return } SplitPath, sFile, _FileExt, _Dir, _Ext, _File, _Drv If ( p[p.length()] = "xInfo" ) ; Last parameter is xInfo { p.Pop() ; Delete parameter fex.SetCapacity(11) ; Make room for Extra info fex["_Attrib"] := A_LoopFileAttrib fex["_Dir"] := _Dir fex["_Drv"] := _Drv fex["_Ext"] := _Ext fex["_File"] := _File fex["_File.Ext"] := _FileExt fex["_FilePath"] := sFile fex["_FileSize"] := A_LoopFileSize fex["_FileTimeA"] := A_LoopFileTimeAccessed fex["_FileTimeC"] := A_LoopFileTimeCreated fex["_FileTimeM"] := A_LoopFileTimeModified } Break } If Not ( _FileExt ) ; Filepath not resolved { Return } objShl := ComObjCreate("Shell.Application") objDir := objShl.NameSpace(_Dir) objItm := objDir.ParseName(_FileExt) If ( VarSetCapacity(xDetails) = 0 ) ; Init static variable { i:=-1, xDetails:={}, xDetails.SetCapacity(309) While ( i++ < 309 ) { xDetails[ objDir.GetDetailsOf(0,i) ] := i } xDetails.Delete("") } If ( Kind and Kind <> objDir.GetDetailsOf(objItm,11) ) ; File isn't desired kind { Return } i:=0, nParams:=p.Count(), fex.SetCapacity(nParams + 11) While ( i++ < nParams ) { Prop := p[i] If ( (Dot:=InStr(Prop,".")) and (Prop:=(Dot=1 ? "System":"") . Prop) ) { fex[Prop] := objItm.ExtendedProperty(Prop) Continue } If ( PropNum := xDetails[Prop] ) > -1 { fex[Prop] := ObjDir.GetDetailsOf(objItm,PropNum) Continue } } fex.SetCapacity(-1) Return fex } ;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ; Misc ;------------------------------------------------ ; Escape::ExitApp ; Functions ;------------------------------------------------ ; Misc ;------------------------------------------------ ; Escape::ExitApp