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 "CaptureDlg.h" 3 4HRESULT CaptureDlg ::show () 5{ 6auto res = DoModal (nullptr ); 7if (res == -1 ) 8return HRESULT_FROM_WIN32 (GetLastError () ); 9switch (res ) 10 { 11case IDC_BACK : 12return SCREEN_MODEL ; 13case IDC_TRANSCRIBE : 14return SCREEN_TRANSCRIBE ; 15 } 16return S_OK ; 17} 18 19static const LPCTSTR regValDevice = L"captureDevice" ; 20static const LPCTSTR regValOutPath = L"captureTextFile" ; 21static const LPCTSTR regValOutFormat = L"captureTextFlags" ; 22 23enum struct CaptureDlg ::eTextFlags :uint32_t 24{ 25Save = 1 , 26Append = 2 , 27Timestamps = 4 , 28}; 29 30LRESULT CaptureDlg ::OnInitDialog (UINT nMessage ,WPARAM wParam ,LPARAM lParam ,BOOL & bHandled ) 31{ 32// First DDX call, hooks up variables to controls. 33DoDataExchange ( false ); 34 35languageSelector .initialize (m_hWnd ,IDC_LANGUAGE ,appState ); 36cbTranslate .initialize (m_hWnd ,IDC_TRANSLATE ,appState ); 37cbConsole .initialize (m_hWnd ,IDC_CONSOLE ,appState ); 38 39pendingState .initialize ( 40// Controls to disable while pending, re-enable afterwards 41 { 42languageSelector ,GetDlgItem (IDC_TRANSLATE ), 43cbCaptureDevice , 44checkSave ,checkAppend ,checkTimestamps ,transcribeOutputPath ,transcribeOutputBrowse , 45GetDlgItem (IDC_DEV_REFRESH ), 46GetDlgItem (IDC_BACK ), 47GetDlgItem (IDC_TRANSCRIBE ), 48GetDlgItem (IDCANCEL ), 49 }, 50// Controls to show while pending, hide afterwards 51 { 52voiceActivity ,GetDlgItem (IDC_VOICE_ACTIVITY_LBL ), 53transcribeActivity ,GetDlgItem (IDC_TRANS_LBL ), 54stalled ,GetDlgItem (IDC_STALL_LBL ), 55progressBar , 56 } ); 57 58stalled .setActiveColor (flipRgb (0xffcc33 ) ); 59 60HRESULT hr = work .create (this ); 61if (FAILED (hr ) ) 62 { 63reportError (m_hWnd ,L"CreateThreadpoolWork failed" ,nullptr ,hr ); 64EndDialog (IDCANCEL ); 65 } 66 67listDevices (); 68selectDevice (appState .stringLoad (regValDevice ) ); 69 70constexpr uint32_t defaultFlags = (uint32_t )eTextFlags::Append ; 71uint32_t flags = appState .dwordLoad (regValOutFormat ,defaultFlags ); 72if (flags & (uint32_t )eTextFlags::Save ) 73checkSave .SetCheck (BST_CHECKED ); 74if (flags & (uint32_t )eTextFlags::Append ) 75checkAppend .SetCheck (BST_CHECKED ); 76if (flags & (uint32_t )eTextFlags::Timestamps ) 77checkTimestamps .SetCheck (BST_CHECKED ); 78 79transcribeOutputPath .SetWindowText (appState .stringLoad (regValOutPath ) ); 80onSaveTextCheckbox (); 81 82appState .lastScreenSave (SCREEN_CAPTURE ); 83appState .setupIcon (this ); 84ATLVERIFY (CenterWindow () ); 85return 0 ; 86} 87 88HRESULT __stdcallCaptureDlg ::listDevicesCallback (int len ,const Whisper ::sCaptureDevice * buffer ,void * pv )noexcept 89{ 90 std::vector < sCaptureDevice >& devices = * ( std::vector < sCaptureDevice > * )pv ; 91devices .resize (len ); 92for (int i = 0 ;i < len ;i ++ ) 93 { 94devices [i ].displayName = buffer [i ].displayName ; 95devices [i ].endpoint = buffer [i ].endpoint ; 96 } 97return S_OK ; 98} 99 100bool CaptureDlg ::listDevices () 101{ 102appState .mediaFoundation -> listCaptureDevices (& listDevicesCallback ,& devices ); 103cbCaptureDevice .ResetContent (); 104for (const auto & dev :devices ) 105cbCaptureDevice .AddString (dev .displayName ); 106return !devices .empty (); 107} 108 109void CaptureDlg ::onDeviceRefresh () 110{ 111// Save the current selection 112const int curSel = cbCaptureDevice .GetCurSel (); 113CString str ; 114if (curSel >=0 && curSel < (int )devices .size () ) 115str = std::move (devices [curSel ].endpoint ); 116 117// Refresh 118listDevices (); 119 120// Restore the selection 121selectDevice (str ); 122 123const size_t len = devices .size (); 124if (len == 0 ) 125 { 126MessageBox (L"No capture devices found on this computer.\nIf you have a USB microphone, connect it to this PC,\nand press �refresh� button." , 127L"Capture Devices" ,MB_OK |MB_ICONWARNING ); 128 } 129else 130 { 131const char * suffix = (len != 1 ) ?"s" :"" ; 132str .Format (L"Detected %zu audio capture device%S." ,len ,suffix ); 133MessageBox (str ,L"Capture Devices" ,MB_OK |MB_ICONINFORMATION ); 134 } 135} 136 137bool CaptureDlg ::selectDevice (LPCTSTR endpoint ) 138{ 139if (nullptr != endpoint && 0 != * endpoint ) 140 { 141for (size_t i = 0 ;i < devices .size ();i ++ ) 142 { 143if (devices [i ].endpoint == endpoint ) 144 { 145cbCaptureDevice .SetCurSel ( (int )i ); 146return true; 147 } 148 } 149 } 150 151if ( !devices .empty () ) 152cbCaptureDevice .SetCurSel (0 ); 153return false; 154} 155 156void CaptureDlg ::onSaveTextCheckbox () 157{ 158const BOOL enabled = (checkSave .GetCheck ()== BST_CHECKED ); 159 std::array < HWND ,4 > controls = {checkAppend ,checkTimestamps ,transcribeOutputPath ,transcribeOutputBrowse }; 160for (HWND w :controls ) 161 ::EnableWindow (w ,enabled ); 162} 163 164void CaptureDlg ::onBrowseResult () 165{ 166LPCTSTR title = L"Output Text File" ; 167LPCTSTR outputFilters = L"Text files (*.txt)\0*.txt\0\0" ; 168CString path ; 169transcribeOutputPath .GetWindowText (path ); 170if ( !getSaveFileName (m_hWnd ,title ,outputFilters ,path ) ) 171return ; 172 173LPCTSTR ext = PathFindExtension (path ); 174if (0 == * ext ) 175 { 176wchar_t * const buffer = path .GetBufferSetLength (path .GetLength ()+ 5 ); 177PathRenameExtension (buffer ,L".txt" ); 178path .ReleaseBuffer (); 179 } 180 181transcribeOutputPath .SetWindowText (path ); 182} 183 184CaptureDlg ::eTextFlags CaptureDlg ::getOutputFlags () 185{ 186uint32_t flags = 0 ; 187if (checkSave .GetCheck ()== BST_CHECKED ) 188flags |= (uint32_t )eTextFlags::Save ; 189if (checkAppend .GetCheck ()== BST_CHECKED ) 190flags |= (uint32_t )eTextFlags::Append ; 191if (checkTimestamps .GetCheck ()== BST_CHECKED ) 192flags |= (uint32_t )eTextFlags::Timestamps ; 193return (eTextFlags )flags ; 194} 195 196void CaptureDlg ::setPending (bool nowPending ) 197{ 198pendingState .setPending (nowPending ); 199if (nowPending ) 200 { 201progressBar .SetMarquee ( TRUE,0 ); 202btnRunCapture .SetWindowText (L"Stop" ); 203 } 204else 205 { 206progressBar .SetMarquee ( FALSE,0 ); 207btnRunCapture .SetWindowText (L"Capture" ); 208btnRunCapture .EnableWindow ( TRUE ); 209captureRunning = false; 210 } 211} 212 213void CaptureDlg ::onRunCapture () 214{ 215if (captureRunning ) 216 { 217threadState .stopRequested = true; 218btnRunCapture .EnableWindow ( FALSE ); 219return ; 220 } 221 222int dev = cbCaptureDevice .GetCurSel (); 223if (dev < 0 || dev >= (int )devices .size () ) 224 { 225showError (L"Please select a capture device" ,S_FALSE ); 226return ; 227 } 228threadState .endpoint = devices [dev ].endpoint ; 229threadState .language = languageSelector .selectedLanguage (); 230threadState .translate = cbTranslate .checked (); 231if (isInvalidTranslate (m_hWnd ,threadState .language ,threadState .translate ) ) 232return ; 233 234threadState .flags = getOutputFlags (); 235if ( (uint32_t )threadState .flags & (uint32_t )eTextFlags::Save ) 236 { 237transcribeOutputPath .GetWindowText (threadState .textOutputPath ); 238if (threadState .textOutputPath .GetLength () <=0 ) 239 { 240showError (L"Please specify the output text file" ,S_FALSE ); 241return ; 242 } 243appState .stringStore (regValOutPath ,threadState .textOutputPath ); 244 } 245else 246cbConsole .ensureChecked (); 247 248languageSelector .saveSelection (appState ); 249cbTranslate .saveSelection (appState ); 250appState .stringStore (regValDevice ,threadState .endpoint ); 251appState .dwordStore (regValOutFormat , (uint32_t )threadState .flags ); 252 253captureRunning = true; 254threadState .errorMessage = L"" ; 255threadState .stopRequested = false; 256threadState .captureParams .minDuration = 7 ; 257threadState .captureParams .maxDuration = 11 ; 258setPending ( true ); 259work .post (); 260} 261 262void __declspec(noinline )CaptureDlg ::getThreadError () 263{ 264getLastError (threadState .errorMessage ); 265} 266 267#define CHECK_EX (hr ) { const HRESULT __hr = ( hr ); if( FAILED( __hr ) ) { getThreadError(); return __hr; } } 268 269static HRESULT appendDate (CString & str ,const SYSTEMTIME & time ) 270{ 271constexpr DWORD dateFlags = DATE_LONGDATE ; 272int cc = GetDateFormatEx (LOCALE_NAME_USER_DEFAULT ,dateFlags ,& time ,nullptr ,nullptr ,0 ,nullptr ); 273if (0 == cc ) 274return getLastHr (); 275 276const int oldLength = str .GetLength (); 277wchar_t * const buffer = str .GetBufferSetLength (oldLength + cc ); 278cc = GetDateFormatEx (LOCALE_NAME_USER_DEFAULT ,dateFlags ,& time ,nullptr ,buffer + oldLength ,cc ,nullptr ); 279if (0 != cc ) 280 { 281str .ReleaseBuffer (); 282return S_OK ; 283 } 284HRESULT hr = getLastHr (); 285str .ReleaseBuffer (); 286return hr ; 287} 288 289static HRESULT appendTime (CString & str ,const SYSTEMTIME & time ) 290{ 291constexpr DWORD timeFlags = 0 ; 292int cc = GetTimeFormatEx (LOCALE_NAME_USER_DEFAULT ,timeFlags ,& time ,nullptr ,nullptr ,0 ); 293if (0 == cc ) 294return getLastHr (); 295 296const int oldLength = str .GetLength (); 297wchar_t * const buffer = str .GetBufferSetLength (oldLength + cc ); 298cc = GetTimeFormatEx (LOCALE_NAME_USER_DEFAULT ,timeFlags ,& time ,nullptr ,buffer + oldLength ,cc ); 299if (0 != cc ) 300 { 301str .ReleaseBuffer (); 302return S_OK ; 303 } 304HRESULT hr = getLastHr (); 305str .ReleaseBuffer (); 306return hr ; 307} 308 309static HRESULT printDateTime (CAtlFile & file ) 310{ 311SYSTEMTIME time ; 312GetLocalTime (& time ); 313 314CString str ; 315str = L"==== Captured on " ; 316CHECK (appendDate (str ,time ) ); 317str += L", " ; 318CHECK (appendTime (str ,time ) ); 319str += L" ====\r\n" ; 320 321CStringA u8 ; 322makeUtf8 (u8 ,str ); 323return file .Write (cstr (u8 ), (DWORD )u8 .GetLength () ); 324} 325 326inline HRESULT CaptureDlg ::runCapture () 327{ 328clearLastError (); 329using namespace Whisper ; 330CComPtr < iAudioCapture > capture ; 331CHECK_EX (appState .mediaFoundation -> openCaptureDevice (threadState .endpoint ,threadState .captureParams ,& capture ) ); 332 333HRESULT hr ; 334CAtlFile file ; 335const uint32_t flags = (uint32_t )threadState .flags ; 336if (flags & (uint32_t )eTextFlags::Save ) 337 { 338const bool append = 0 != (flags & (uint32_t )eTextFlags::Append ); 339const DWORD creation = append ?OPEN_ALWAYS :CREATE_ALWAYS ; 340hr = file .Create (threadState .textOutputPath ,GENERIC_WRITE ,FILE_SHARE_READ ,creation ); 341if (FAILED (hr ) ) 342 { 343threadState .errorMessage = L"Unable to create the output text file" ; 344return hr ; 345 } 346if (append ) 347 { 348ULONGLONG size ; 349CHECK (file .GetSize (size ) ); 350if (size == 0 ) 351CHECK (writeUtf8Bom (file ) ) 352else 353CHECK (file .Seek (0 ,SEEK_END ) ); 354 } 355else 356 { 357CHECK (writeUtf8Bom (file ) ); 358 } 359 360if (flags & (uint32_t )eTextFlags::Timestamps ) 361CHECK (printDateTime (file ) ); 362 363threadState .file = & file ; 364 } 365else 366threadState .file = nullptr ; 367 368CComPtr < iContext > context ; 369CHECK_EX (appState .model -> createContext (& context ) ); 370 371sFullParams fullParams ; 372CHECK_EX (context -> fullDefaultParams ( eSamplingStrategy::Greedy ,& fullParams ) ); 373fullParams .language = threadState .language ; 374fullParams .setFlag ( eFullParamsFlags::Translate ,threadState .translate ); 375fullParams .resetFlag ( eFullParamsFlags::PrintRealtime ); 376fullParams .new_segment_callback = & newSegmentCallback ; 377fullParams .new_segment_callback_user_data = this ; 378 379sCaptureCallbacks callbacks ; 380callbacks .shouldCancel = & cbCancel ; 381callbacks .captureStatus = & cbStatus ; 382callbacks .pv = this ; 383 384CHECK_EX (context -> runCapture (fullParams ,callbacks ,capture ) ); 385threadState .file = nullptr ; 386 387context -> timingsPrint (); 388return S_OK ; 389} 390 391void __stdcallCaptureDlg ::poolCallback ()noexcept 392{ 393const HRESULT hr = runCapture (); 394PostMessage (WM_CALLBACK_COMPLETION ,hr ); 395} 396 397void CaptureDlg ::showError (LPCTSTR text ,HRESULT hr ) 398{ 399reportError (m_hWnd ,text ,L"Capture failed" ,hr ); 400} 401 402LRESULT CaptureDlg ::onThreadQuit (UINT nMessage ,WPARAM wParam ,LPARAM lParam ,BOOL & bHandled ) 403{ 404setPending ( false ); 405 406const HRESULT hr = (HRESULT )wParam ; 407if (FAILED (hr ) ) 408 { 409LPCTSTR failMessage = L"Capture failed" ; 410 411if (threadState .errorMessage .GetLength ()> 0 ) 412 { 413CString tmp = failMessage ; 414tmp += L"\n" ; 415tmp += threadState .errorMessage ; 416showError (tmp ,hr ); 417 } 418else 419showError (failMessage ,hr ); 420 421return 0 ; 422 } 423else 424 { 425if ( (uint32_t )threadState .flags & (uint32_t )eTextFlags::Save ) 426ShellExecute (NULL ,L"open" ,threadState .textOutputPath ,NULL ,NULL ,SW_SHOW ); 427 } 428 429return 0 ; 430} 431 432LRESULT CaptureDlg ::onThreadStatus (UINT nMessage ,WPARAM wParam ,LPARAM lParam ,BOOL & bHandled ) 433{ 434using namespace Whisper ; 435const uint8_t newStatus = (uint8_t )wParam ; 436// Update the GUI 437voiceActivity .setActive (0 != (newStatus & (uint8_t )eCaptureStatus::Voice ) ); 438transcribeActivity .setActive (0 != (newStatus & (uint8_t )eCaptureStatus::Transcribing ) ); 439stalled .setActive (0 != (newStatus & (uint8_t )eCaptureStatus::Stalled ) ); 440return 0 ; 441} 442 443HRESULT __stdcallCaptureDlg ::cbCancel (void * pv )noexcept 444{ 445const bool stopRequested = ( (CaptureDlg * )pv )-> threadState .stopRequested ; 446return stopRequested ?S_FALSE :S_OK ; 447} 448 449HRESULT __stdcallCaptureDlg ::cbStatus (void * pv ,Whisper ::eCaptureStatus status )noexcept 450{ 451CaptureDlg & dialog = * (CaptureDlg * )pv ; 452if (dialog .PostMessage (WM_CALLBACK_STATUS , (uint8_t )status ) ) 453return S_OK ; 454return getLastHr (); 455} 456 457HRESULT __cdeclCaptureDlg ::newSegmentCallback (Whisper ::iContext * ctx ,uint32_t n_new ,void * user_data )noexcept 458{ 459using namespace Whisper ; 460CComPtr < iTranscribeResult > result ; 461const eResultFlags flags = eResultFlags::Timestamps | eResultFlags::Tokens ; 462CHECK (ctx -> getResults (flags ,& result ) ); 463CHECK (logNewSegments (result ,n_new ) ); 464 465CaptureDlg & dialog = * (CaptureDlg * )user_data ; 466return dialog .appendTextFile (result ,n_new ); 467} 468 469HRESULT CaptureDlg ::appendTextFile (Whisper ::iTranscribeResult * results ,uint32_t newSegments ) 470{ 471if (nullptr == threadState .file || 0 == newSegments ) 472return S_OK ; 473 474using namespace Whisper ; 475sTranscribeLength length ; 476CHECK (results -> getSize (length ) ); 477 478const size_t len = length .countSegments ; 479size_t i = len - newSegments ; 480 481const sSegment * const segments = results -> getSegments (); 482CStringA str ; 483for ( ;i < len ;i ++ ) 484 { 485const sSegment & seg = segments [i ]; 486if (0 != ( (uint32_t )threadState .flags & (uint32_t )eTextFlags::Timestamps ) ) 487 { 488str = "[" ; 489printTime (str ,seg .time .begin ); 490str += " --> " ; 491printTime (str ,seg .time .end ); 492str += "] " ; 493 } 494else 495str = "" ; 496 497str += seg .text ; 498str += "\r\n" ; 499 500CHECK (threadState .file -> Write (cstr (str ), (DWORD )str .GetLength () ) ); 501 } 502 503CHECK (threadState .file -> Flush () ); 504return S_OK ; 505}