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