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

KonstantinRefactor, removed a redundant function3f3a9a1

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