yum-archive/TaSTT-Whisper
High-performance GPGPU inference of OpenAI's Whisper automatic speech recognition (ASR) model
git clone https://git.yummers.dev/yum-archive/TaSTT-Whisper
3f3a9a1
master
1#include "stdafx.h" 2#include "TranscribeDlg.h" 3#include "Utils/logger.h" 4 5HRESULT TranscribeDlg ::show () 6{ 7auto res = DoModal (nullptr ); 8if (res == -1 ) 9return HRESULT_FROM_WIN32 (GetLastError () ); 10switch (res ) 11 { 12case IDC_BACK : 13return SCREEN_MODEL ; 14case IDC_CAPTURE : 15return SCREEN_CAPTURE ; 16 } 17return S_OK ; 18} 19 20constexpr int progressMaxInteger = 1024 * 8 ; 21 22static const LPCTSTR regValInput = L"sourceMedia" ; 23static const LPCTSTR regValOutFormat = L"resultFormat" ; 24static const LPCTSTR regValOutPath = L"resultPath" ; 25static const LPCTSTR regValUseInputFolder = L"useInputFolder" ; 26 27LRESULT TranscribeDlg ::OnInitDialog (UINT nMessage ,WPARAM wParam ,LPARAM lParam ,BOOL & bHandled ) 28{ 29// First DDX call, hooks up variables to controls. 30DoDataExchange ( false ); 31printModelDescription (); 32languageSelector .initialize (m_hWnd ,IDC_LANGUAGE ,appState ); 33cbConsole .initialize (m_hWnd ,IDC_CONSOLE ,appState ); 34cbTranslate .initialize (m_hWnd ,IDC_TRANSLATE ,appState ); 35populateOutputFormats (); 36 37pendingState .initialize ( 38 { 39languageSelector ,GetDlgItem (IDC_TRANSLATE ), 40sourceMediaPath ,GetDlgItem (IDC_BROWSE_MEDIA ), 41transcribeOutFormat ,useInputFolder , 42transcribeOutputPath ,GetDlgItem (IDC_BROWSE_RESULT ), 43GetDlgItem (IDC_TRANSCRIBE ), 44GetDlgItem (IDCANCEL ), 45GetDlgItem (IDC_BACK ), 46GetDlgItem (IDC_CAPTURE ) 47 }, 48 { 49progressBar ,GetDlgItem (IDC_PENDING_TEXT ) 50 } ); 51 52HRESULT hr = work .create (this ); 53if (FAILED (hr ) ) 54 { 55reportError (m_hWnd ,L"CreateThreadpoolWork failed" ,nullptr ,hr ); 56EndDialog (IDCANCEL ); 57 } 58 59progressBar .SetRange32 (0 ,progressMaxInteger ); 60progressBar .SetStep (1 ); 61 62sourceMediaPath .SetWindowText (appState .stringLoad (regValInput ) ); 63transcribeOutFormat .SetCurSel ( (int )appState .dwordLoad (regValOutFormat ,0 ) ); 64transcribeOutputPath .SetWindowText (appState .stringLoad (regValOutPath ) ); 65if (appState .boolLoad (regValUseInputFolder ) ) 66useInputFolder .SetCheck (BST_CHECKED ); 67BOOL unused ; 68onOutFormatChange (0 ,0 ,nullptr ,unused ); 69 70appState .lastScreenSave (SCREEN_TRANSCRIBE ); 71appState .setupIcon (this ); 72ATLVERIFY (CenterWindow () ); 73return 0 ; 74} 75 76void TranscribeDlg ::printModelDescription () 77{ 78CString text ; 79if (S_OK == appState .model -> isMultilingual () ) 80text = L"Multilingual" ; 81else 82text = L"Single-language" ; 83text += L" model \"" ; 84LPCTSTR path = appState .source .path ; 85path = ::PathFindFileName (path ); 86text += path ; 87text += L"\", " ; 88const int64_t cb = appState .source .sizeInBytes ; 89if (cb < 1 <<30 ) 90 { 91constexpr double mul = 1.0 / (1 <<20 ); 92double mb = (double )cb * mul ; 93text .AppendFormat (L"%.1f MB" ,mb ); 94 } 95else 96 { 97constexpr double mul = 1.0 / (1 <<30 ); 98double gb = (double )cb * mul ; 99text .AppendFormat (L"%.2f GB" ,gb ); 100 } 101text += L" on disk, " ; 102text += implString (appState .source .impl ); 103text += L" implementation" ; 104 105modelDesc .SetWindowText (text ); 106} 107 108// Populate the "Output Format" combobox 109void TranscribeDlg ::populateOutputFormats () 110{ 111transcribeOutFormat .AddString (L"None" ); 112transcribeOutFormat .AddString (L"Text file" ); 113transcribeOutFormat .AddString (L"Text with timestamps" ); 114transcribeOutFormat .AddString (L"SubRip subtitles" ); 115transcribeOutFormat .AddString (L"WebVTT subtitles" ); 116} 117 118// The enum values should match 0-based indices of the combobox items 119enum struct TranscribeDlg ::eOutputFormat :uint8_t 120{ 121None = 0 , 122Text = 1 , 123TextTimestamps = 2 , 124SubRip = 3 , 125WebVTT = 4 , 126}; 127 128// CBN_SELCHANGE notification for IDC_OUTPUT_FORMAT combobox 129LRESULT TranscribeDlg ::onOutFormatChange (UINT ,INT ,HWND ,BOOL & bHandled ) 130{ 131BOOL enabled = transcribeOutFormat .GetCurSel ()!= 0 ; 132useInputFolder .EnableWindow (enabled ); 133 134if (isChecked (useInputFolder )&& enabled ) 135 { 136enabled = FALSE; 137setOutputPath (); 138 } 139transcribeOutputPath .EnableWindow (enabled ); 140transcribeOutputBrowse .EnableWindow (enabled ); 141 142return 0 ; 143} 144 145// EN_CHANGE notification for IDC_PATH_MEDIA edit box 146LRESULT TranscribeDlg ::onInputChange (UINT ,INT ,HWND ,BOOL & ) 147{ 148if ( !useInputFolder .IsWindowEnabled () ) 149return 0 ; 150if ( !isChecked (useInputFolder ) ) 151return 0 ; 152setOutputPath (); 153return 0 ; 154} 155 156void TranscribeDlg ::onBrowseMedia () 157{ 158LPCTSTR title = L"Input audio file to transcribe" ; 159LPCTSTR filters = L"Multimedia Files\0*.wav;*.wave;*.mp3;*.wma;*.mp4;*.mpeg4;*.mkv;*.m4a\0\0" ; 160 161CString path ; 162sourceMediaPath .GetWindowText (path ); 163if ( !getOpenFileName (m_hWnd ,title ,filters ,path ) ) 164return ; 165sourceMediaPath .SetWindowText (path ); 166if (useInputFolder .IsWindowEnabled ()&& useInputFolder .GetCheck ()== BST_CHECKED ) 167setOutputPath (path ); 168} 169 170static const LPCTSTR outputFilters = L"Text files (*.txt)\0*.txt\0Text with timestamps (*.txt)\0*.txt\0SubRip subtitles (*.srt)\0*.srt\0WebVTT subtitles (*.vtt)\0*.vtt\0\0" ; 171static const std::array < LPCTSTR ,4 > outputExtensions = 172{ 173L".txt" ,L".txt" ,L".srt" ,L".vtt" 174}; 175 176void TranscribeDlg ::setOutputPath (const CString & input ) 177{ 178const int format = transcribeOutFormat .GetCurSel ()- 1 ; 179if (format < 0 || format >=outputExtensions .size () ) 180return ; 181const LPCTSTR ext = outputExtensions [format ]; 182CString path = input ; 183path .Trim (); 184const bool renamed = PathRenameExtension (path .GetBufferSetLength (path .GetLength ()+ 4 ),ext ); 185path .ReleaseBuffer (); 186if ( !renamed ) 187return ; 188transcribeOutputPath .SetWindowText (path ); 189} 190 191void TranscribeDlg ::setOutputPath () 192{ 193CString path ; 194if ( !sourceMediaPath .GetWindowText (path ) ) 195return ; 196if (path .GetLength () <=0 ) 197return ; 198setOutputPath (path ); 199} 200 201void TranscribeDlg ::onInputFolderCheck () 202{ 203const bool checked = isChecked (useInputFolder ); 204 205BOOL enableOutput = checked ? FALSE : TRUE; 206transcribeOutputPath .EnableWindow (enableOutput ); 207transcribeOutputBrowse .EnableWindow (enableOutput ); 208 209if ( !checked ) 210return ; 211setOutputPath (); 212} 213 214void TranscribeDlg ::onBrowseOutput () 215{ 216const DWORD origFilterIndex = (DWORD )transcribeOutFormat .GetCurSel ()- 1 ; 217 218LPCTSTR title = L"Output Text File" ; 219CString path ; 220transcribeOutputPath .GetWindowText (path ); 221DWORD filterIndex = origFilterIndex ; 222if ( !getSaveFileName (m_hWnd ,title ,outputFilters ,path ,& filterIndex ) ) 223return ; 224 225LPCTSTR ext = PathFindExtension (path ); 226if (0 == * ext && filterIndex < outputExtensions .size () ) 227 { 228wchar_t * const buffer = path .GetBufferSetLength (path .GetLength ()+ 5 ); 229PathRenameExtension (buffer ,outputExtensions [filterIndex ] ); 230path .ReleaseBuffer (); 231 } 232 233transcribeOutputPath .SetWindowText (path ); 234if (filterIndex != origFilterIndex ) 235transcribeOutFormat .SetCurSel (filterIndex + 1 ); 236} 237 238void TranscribeDlg ::setPending (bool nowPending ) 239{ 240pendingState .setPending (nowPending ); 241} 242 243void TranscribeDlg ::transcribeError (LPCTSTR text ,HRESULT hr ) 244{ 245reportError (m_hWnd ,text ,L"Unable to transcribe audio" ,hr ); 246} 247 248void TranscribeDlg ::onTranscribe () 249{ 250// Validate input 251sourceMediaPath .GetWindowText (transcribeArgs .pathMedia ); 252if (transcribeArgs .pathMedia .GetLength () <=0 ) 253 { 254transcribeError (L"Please select an input audio file" ); 255return ; 256 } 257 258if ( !PathFileExists (transcribeArgs .pathMedia ) ) 259 { 260transcribeError (L"Input audio file does not exist" ,HRESULT_FROM_WIN32 (ERROR_FILE_NOT_FOUND ) ); 261return ; 262 } 263 264transcribeArgs .language = languageSelector .selectedLanguage (); 265transcribeArgs .translate = cbTranslate .checked (); 266if (isInvalidTranslate (m_hWnd ,transcribeArgs .language ,transcribeArgs .translate ) ) 267return ; 268 269transcribeArgs .format = (eOutputFormat )(uint8_t )transcribeOutFormat .GetCurSel (); 270if (transcribeArgs .format != eOutputFormat::None ) 271 { 272transcribeOutputPath .GetWindowText (transcribeArgs .pathOutput ); 273if (transcribeArgs .pathOutput .GetLength () <=0 ) 274 { 275transcribeError (L"Please select an output text file" ); 276return ; 277 } 278if (PathFileExists (transcribeArgs .pathOutput ) ) 279 { 280const int resp = MessageBox (L"The output file is already there.\nOverwrite the file?" ,L"Confirm Overwrite" ,MB_ICONQUESTION |MB_YESNO ); 281if (resp != IDYES ) 282return ; 283 } 284appState .stringStore (regValOutPath ,transcribeArgs .pathOutput ); 285 } 286else 287cbConsole .ensureChecked (); 288 289appState .dwordStore (regValOutFormat , (uint32_t )(int )transcribeArgs .format ); 290appState .boolStore (regValUseInputFolder ,isChecked (useInputFolder ) ); 291languageSelector .saveSelection (appState ); 292cbTranslate .saveSelection (appState ); 293appState .stringStore (regValInput ,transcribeArgs .pathMedia ); 294 295setPending ( true ); 296 297work .post (); 298} 299 300void __stdcallTranscribeDlg ::poolCallback ()noexcept 301{ 302HRESULT hr = transcribe (); 303PostMessage (WM_CALLBACK_STATUS , (WPARAM )hr ); 304} 305 306static void printTime (CString & rdi ,int64_t ticks ) 307{ 308const Whisper ::sTimeSpan ts { (uint64_t )ticks }; 309const Whisper ::sTimeSpanFields fields = ts ; 310 311if (fields .days != 0 ) 312 { 313rdi .AppendFormat (L"%i days, %i hours" ,fields .days , (int )fields .hours ); 314return ; 315 } 316if ( (fields .hours |fields .minutes )!= 0 ) 317 { 318rdi .AppendFormat (L"%02d:%02d:%02d" , (int )fields .hours , (int )fields .minutes , (int )fields .seconds ); 319return ; 320 } 321rdi .AppendFormat (L"%.3f seconds" , (double )ticks /1E7 ); 322} 323 324LRESULT TranscribeDlg ::onCallbackStatus (UINT ,WPARAM wParam ,LPARAM ,BOOL & bHandled ) 325{ 326setPending ( false ); 327const HRESULT hr = (HRESULT )wParam ; 328if (FAILED (hr ) ) 329 { 330LPCTSTR failMessage = L"Transcribe failed" ; 331 332if (transcribeArgs .errorMessage .GetLength ()> 0 ) 333 { 334CString tmp = failMessage ; 335tmp += L"\n" ; 336tmp += transcribeArgs .errorMessage ; 337transcribeError (tmp ,hr ); 338 } 339else 340transcribeError (failMessage ,hr ); 341 342return 0 ; 343 } 344 345const int64_t elapsed = (GetTickCount64 ()- transcribeArgs .startTime )* 10'000 ; 346const int64_t media = transcribeArgs .mediaDuration ; 347CString message = L"Transcribed the audio\nMedia duration: " ; 348printTime (message ,media ); 349message += L"\nProcessing time: " ; 350printTime (message ,elapsed ); 351message += L"\nRelative processing speed: " ; 352double mul = (double )media / (double )elapsed ; 353message .AppendFormat (L"%g" ,mul ); 354 355MessageBox (message ,L"Transcribe Completed" ,MB_OK |MB_ICONINFORMATION ); 356return 0 ; 357} 358 359void TranscribeDlg ::getThreadError () 360{ 361getLastError (transcribeArgs .errorMessage ); 362} 363 364#define CHECK_EX (hr ) { const HRESULT __hr = ( hr ); if( FAILED( __hr ) ) { getThreadError(); return __hr; } } 365 366HRESULT TranscribeDlg ::transcribe () 367{ 368transcribeArgs .startTime = GetTickCount64 (); 369clearLastError (); 370transcribeArgs .errorMessage = L"" ; 371 372using namespace Whisper ; 373CComPtr < iAudioReader > reader ; 374 375CHECK_EX (appState .mediaFoundation -> openAudioFile (transcribeArgs .pathMedia , false,& reader ) ); 376 377const eOutputFormat format = transcribeArgs .format ; 378CAtlFile outputFile ; 379if (format != eOutputFormat::None ) 380CHECK (outputFile .Create (transcribeArgs .pathOutput ,GENERIC_WRITE ,0 ,CREATE_ALWAYS ) ); 381 382transcribeArgs .resultFlags = eResultFlags::Timestamps | eResultFlags::Tokens ; 383 384CComPtr < iContext > context ; 385CHECK_EX (appState .model -> createContext (& context ) ); 386 387sFullParams fullParams ; 388CHECK_EX (context -> fullDefaultParams ( eSamplingStrategy::Greedy ,& fullParams ) ); 389fullParams .language = transcribeArgs .language ; 390fullParams .setFlag ( eFullParamsFlags::Translate ,transcribeArgs .translate ); 391fullParams .resetFlag ( eFullParamsFlags::PrintRealtime ); 392 393fullParams .new_segment_callback_user_data = this ; 394fullParams .new_segment_callback = & newSegmentCallbackStatic ; 395 396// Setup the progress indication sink 397sProgressSink progressSink {& progressCallbackStatic ,this }; 398// Run the transcribe 399CHECK_EX (context -> runStreamed (fullParams ,progressSink ,reader ) ); 400 401// Once finished, query duration of the audio. 402// The duration before the processing is sometimes different, by 20 seconds for the file in that issue: 403// https://github.com/Const-me/Whisper/issues/4 404CHECK_EX (reader -> getDuration (transcribeArgs .mediaDuration ) ); 405 406context -> timingsPrint (); 407 408if (format == eOutputFormat::None ) 409return S_OK ; 410 411CComPtr < iTranscribeResult > result ; 412CHECK_EX (context -> getResults (transcribeArgs .resultFlags ,& result ) ); 413 414sTranscribeLength len ; 415CHECK_EX (result -> getSize (len ) ); 416const sSegment * const segments = result -> getSegments (); 417 418switch (format ) 419 { 420case eOutputFormat::Text : 421return writeTextFile (segments ,len .countSegments ,outputFile , false ); 422case eOutputFormat::TextTimestamps : 423return writeTextFile (segments ,len .countSegments ,outputFile , true ); 424case eOutputFormat::SubRip : 425return writeSubRip (segments ,len .countSegments ,outputFile ); 426case eOutputFormat::WebVTT : 427return writeWebVTT (segments ,len .countSegments ,outputFile ); 428default : 429return E_FAIL ; 430 } 431} 432 433#undef CHECK_EX 434 435inline HRESULT TranscribeDlg ::progressCallback (double p )noexcept 436{ 437constexpr double mul = progressMaxInteger ; 438int pos = lround (mul * p ); 439progressBar .PostMessage (PBM_SETPOS ,pos ,0 ); 440return S_OK ; 441} 442 443HRESULT __cdeclTranscribeDlg ::progressCallbackStatic (double p ,Whisper ::iContext * ctx ,void * pv )noexcept 444{ 445TranscribeDlg * dlg = (TranscribeDlg * )pv ; 446return dlg -> progressCallback (p ); 447} 448 449namespace 450{ 451HRESULT write (CAtlFile & file ,const CStringA & line ) 452 { 453if (line .GetLength ()> 0 ) 454CHECK (file .Write (cstr (line ), (DWORD )line .GetLength () ) ); 455return S_OK ; 456 } 457 458const char * skipBlank (const char * rsi ) 459 { 460while ( true ) 461 { 462const char c = * rsi ; 463if (c == ' ' || c == '\t' ) 464 { 465rsi ++ ; 466continue ; 467 } 468return rsi ; 469 } 470 } 471} 472 473using Whisper ::sSegment ; 474 475 476HRESULT TranscribeDlg ::writeTextFile (const sSegment * const segments ,const size_t length ,CAtlFile & file ,bool timestamps ) 477{ 478using namespace Whisper ; 479CHECK (writeUtf8Bom (file ) ); 480CStringA line ; 481for (size_t i = 0 ;i < length ;i ++ ) 482 { 483const sSegment & seg = segments [i ]; 484 485if (timestamps ) 486 { 487line = "[" ; 488printTime (line ,seg .time .begin ); 489line += " --> " ; 490printTime (line ,seg .time .end ); 491line += "] " ; 492 } 493else 494line = "" ; 495 496line += skipBlank (seg .text ); 497line += "\r\n" ; 498CHECK (write (file ,line ) ); 499 } 500return S_OK ; 501} 502 503HRESULT TranscribeDlg ::writeSubRip (const sSegment * const segments ,const size_t length ,CAtlFile & file ) 504{ 505CHECK (writeUtf8Bom (file ) ); 506CStringA line ; 507for (size_t i = 0 ;i < length ;i ++ ) 508 { 509const sSegment & seg = segments [i ]; 510 511line .Format ("%zu\r\n" ,i + 1 ); 512printTime (line ,seg .time .begin , true ); 513line += " --> " ; 514printTime (line ,seg .time .end , true ); 515line += "\r\n" ; 516line += skipBlank (seg .text ); 517line += "\r\n\r\n" ; 518CHECK (write (file ,line ) ); 519 } 520return S_OK ; 521} 522 523HRESULT TranscribeDlg ::writeWebVTT (const sSegment * const segments ,const size_t length ,CAtlFile & file ) 524{ 525CHECK (writeUtf8Bom (file ) ); 526CStringA line ; 527line = "WEBVTT\r\n\r\n" ; 528CHECK (write (file ,line ) ); 529 530for (size_t i = 0 ;i < length ;i ++ ) 531 { 532const sSegment & seg = segments [i ]; 533line = "" ; 534 535printTime (line ,seg .time .begin , false ); 536line += " --> " ; 537printTime (line ,seg .time .end , false ); 538line += "\r\n" ; 539line += skipBlank (seg .text ); 540line += "\r\n\r\n" ; 541CHECK (write (file ,line ) ); 542 } 543return S_OK ; 544} 545 546inline HRESULT TranscribeDlg ::newSegmentCallback (Whisper ::iContext * ctx ,uint32_t n_new ) 547{ 548using namespace Whisper ; 549CComPtr < iTranscribeResult > result ; 550CHECK (ctx -> getResults (transcribeArgs .resultFlags ,& result ) ); 551return logNewSegments (result ,n_new ); 552} 553 554HRESULT __cdeclTranscribeDlg ::newSegmentCallbackStatic (Whisper ::iContext * ctx ,uint32_t n_new ,void * user_data )noexcept 555{ 556TranscribeDlg * dlg = (TranscribeDlg * )user_data ; 557return dlg -> newSegmentCallback (ctx ,n_new ); 558} 559 560void TranscribeDlg ::onWmClose () 561{ 562if (GetDlgItem (IDCANCEL ).IsWindowEnabled () ) 563 { 564EndDialog (IDCANCEL ); 565return ; 566 } 567 568constexpr UINT flags = MB_YESNO |MB_ICONQUESTION |MB_DEFBUTTON2 ; 569const int res = this -> MessageBox (L"Transcribe is in progress.\nDo you want to quit anyway?" ,L"Confirm exit" ,flags ); 570if (res != IDYES ) 571return ; 572 573// TODO: instead of ExitProcess(), implement another callback in the DLL API, for proper cancellation of the background task 574ExitProcess (1 ); 575}