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

KonstantinWhen token timestamps are requested, disabled streaming in C++ CLI examplee736f91

master
9.6 KiB328 linesraw
1#include "params.h"
2#include "../../Whisper/API/iContext.cl.h"
3#include "../../Whisper/API/iMediaFoundation.cl.h"
4#include "../../ComLightLib/comLightClient.h"
5#include "miscUtils.h"
6#include <array>
7#include <atomic>
8#include "textWriter.h"
9using namespace Whisper;
10
11#define STREAM_AUDIO 1
12
13static HRESULT loadWhisperModel( const wchar_t* path, iModel** pp )
14{
15	using namespace Whisper;
16	constexpr eModelImplementation impl = eModelImplementation::GPU;
17	// constexpr eModelImplementation impl = eModelImplementation::Reference;
18	constexpr uint32_t flags = 0;
19	return Whisper::loadModel( path, impl, flags, nullptr, pp );
20}
21
22namespace
23{
24	// Terminal color map. 10 colors grouped in ranges [0.0, 0.1, ..., 0.9]
25	// Lowest is red, middle is yellow, highest is green.
26	static const std::array<const char*, 10> k_colors =
27	{
28		"\033[38;5;196m", "\033[38;5;202m", "\033[38;5;208m", "\033[38;5;214m", "\033[38;5;220m",
29		"\033[38;5;226m", "\033[38;5;190m", "\033[38;5;154m", "\033[38;5;118m", "\033[38;5;82m",
30	};
31
32	std::string to_timestamp( sTimeSpan ts, bool comma = false )
33	{
34		sTimeSpanFields fields = ts;
35		uint32_t msec = fields.ticks / 10'000;
36		uint32_t hr = fields.days * 24 + fields.hours;
37		uint32_t min = fields.minutes;
38		uint32_t sec = fields.seconds;
39
40		char buf[ 32 ];
41		snprintf( buf, sizeof( buf ), "%02d:%02d:%02d%s%03d", hr, min, sec, comma ? "," : ".", msec );
42		return std::string( buf );
43	}
44
45	static int colorIndex( const sToken& tok )
46	{
47		const float p = tok.probability;
48		const float p3 = p * p * p;
49		int col = (int)( p3 * float( k_colors.size() ) );
50		col = std::max( 0, std::min( (int)k_colors.size() - 1, col ) );
51		return col;
52	}
53
54	HRESULT __cdecl newSegmentCallback( iContext* context, uint32_t n_new, void* user_data ) noexcept
55	{
56		ComLight::CComPtr<iTranscribeResult> results;
57		CHECK( context->getResults( eResultFlags::Timestamps | eResultFlags::Tokens, &results ) );
58
59		sTranscribeLength length;
60		CHECK( results->getSize( length ) );
61
62		const whisper_params& params = *( (const whisper_params*)user_data );
63
64		// print the last n_new segments
65		const uint32_t s0 = length.countSegments - n_new;
66		if( s0 == 0 )
67			printf( "\n" );
68
69		const sSegment* const segments = results->getSegments();
70		const sToken* const tokens = results->getTokens();
71
72		for( uint32_t i = s0; i < length.countSegments; i++ )
73		{
74			const sSegment& seg = segments[ i ];
75
76			if( params.no_timestamps )
77			{
78				if( params.print_colors )
79				{
80					for( uint32_t j = 0; j < seg.countTokens; j++ )
81					{
82						const sToken& tok = tokens[ seg.firstToken + j ];
83						if( !params.print_special && ( tok.flags & eTokenFlags::Special ) )
84							continue;
85						wprintf( L"%S%s%S", k_colors[ colorIndex( tok ) ], utf16( tok.text ).c_str(), "\033[0m" );
86					}
87				}
88				else
89					wprintf( L"%s", utf16( seg.text ).c_str() );
90				fflush( stdout );
91				continue;
92			}
93
94			std::string speaker = "";
95
96			if( params.diarize )
97			{
98				eSpeakerChannel channel;
99				HRESULT hr = context->detectSpeaker( seg.time, channel );
100				if( SUCCEEDED( hr ) && channel != eSpeakerChannel::NoStereoData )
101				{
102					using namespace std::string_literals;
103					switch( channel )
104					{
105					case eSpeakerChannel::Unsure:
106						speaker = "(speaker ?)"s;
107						break;
108					case eSpeakerChannel::Left:
109						speaker = "(speaker 0)"s;
110						break;
111					case eSpeakerChannel::Right:
112						speaker = "(speaker 1)";
113						break;
114					}
115				}
116			}
117
118			if( params.print_colors )
119			{
120				printf( "[%s --> %s] %s ",
121					to_timestamp( seg.time.begin ).c_str(),
122					to_timestamp( seg.time.end ).c_str(),
123					speaker.c_str() );
124
125				for( uint32_t j = 0; j < seg.countTokens; j++ )
126				{
127					const sToken& tok = tokens[ seg.firstToken + j ];
128					if( !params.print_special && ( tok.flags & eTokenFlags::Special ) )
129						continue;
130					wprintf( L"%S%s%S", k_colors[ colorIndex( tok ) ], utf16( tok.text ).c_str(), "\033[0m" );
131				}
132				printf( "\n" );
133			}
134			else
135				wprintf( L"[%S --> %S]  %S%s\n", to_timestamp( seg.time.begin ).c_str(), to_timestamp( seg.time.end ).c_str(), speaker.c_str(), utf16( seg.text ).c_str() );
136		}
137		return S_OK;
138	}
139
140	HRESULT __cdecl beginSegmentCallback( iContext* context, void* user_data ) noexcept
141	{
142		std::atomic_bool* flag = (std::atomic_bool*)user_data;
143		bool aborted = flag->load();
144		return aborted ? S_FALSE : S_OK;
145	}
146
147	HRESULT setupConsoleColors()
148	{
149		HANDLE h = GetStdHandle( STD_OUTPUT_HANDLE );
150		if( h == INVALID_HANDLE_VALUE )
151			return HRESULT_FROM_WIN32( GetLastError() );
152
153		DWORD mode = 0;
154		if( !GetConsoleMode( h, &mode ) )
155			return HRESULT_FROM_WIN32( GetLastError() );
156		if( 0 != ( mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING ) )
157			return S_FALSE;
158
159		mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
160		if( !SetConsoleMode( h, mode ) )
161			return HRESULT_FROM_WIN32( GetLastError() );
162		return S_OK;
163	}
164}
165
166int wmain( int argc, wchar_t* argv[] )
167{
168	// Whisper::dbgCompareTraces( LR"(C:\Temp\2remove\Whisper\ref.bin)", LR"(C:\Temp\2remove\Whisper\gpu.bin )" ); return 0;
169
170	// Tell logger to use the standard output stream for the messages
171	{
172		Whisper::sLoggerSetup logSetup;
173		logSetup.flags = eLoggerFlags::UseStandardError;
174		logSetup.level = eLogLevel::Debug;
175		Whisper::setupLogger( logSetup );
176	}
177
178	whisper_params params;
179	if( !params.parse( argc, argv ) )
180		return 1;
181
182	if( params.print_colors )
183	{
184		if( FAILED( setupConsoleColors() ) )
185			params.print_colors = false;
186	}
187
188	if( params.fname_inp.empty() )
189	{
190		fprintf( stderr, "error: no input files specified\n" );
191		whisper_print_usage( argc, argv, params );
192		return 2;
193	}
194
195	if( Whisper::findLanguageKeyA( params.language.c_str() ) == UINT_MAX )
196	{
197		fprintf( stderr, "error: unknown language '%s'\n", params.language.c_str() );
198		whisper_print_usage( argc, argv, params );
199		return 3;
200	}
201
202	ComLight::CComPtr<iModel> model;
203	HRESULT hr = loadWhisperModel( params.model.c_str(), &model );
204	if( FAILED( hr ) )
205	{
206		printError( "failed to load the model", hr );
207		return 4;
208	}
209
210	ComLight::CComPtr<iContext> context;
211	hr = model->createContext( &context );
212	if( FAILED( hr ) )
213	{
214		printError( "failed to initialize whisper context", hr );
215		return 5;
216	}
217
218	ComLight::CComPtr<iMediaFoundation> mf;
219	hr = initMediaFoundation( &mf );
220	if( FAILED( hr ) )
221	{
222		printError( "failed to initialize Media Foundation runtime", hr );
223		return 5;
224	}
225
226	for( const std::wstring& fname : params.fname_inp )
227	{
228		// print some info about the processing
229		{
230			if( model->isMultilingual() == S_FALSE )
231			{
232				if( params.language != "en" || params.translate )
233				{
234					params.language = "en";
235					params.translate = false;
236					fprintf( stderr, "%s: WARNING: model is not multilingual, ignoring language and translation options\n", __func__ );
237				}
238			}
239		}
240
241		// run the inference
242		Whisper::sFullParams wparams;
243		context->fullDefaultParams( eSamplingStrategy::Greedy, &wparams );
244
245		wparams.resetFlag( eFullParamsFlags::PrintRealtime | eFullParamsFlags::PrintProgress );
246		wparams.setFlag( eFullParamsFlags::PrintTimestamps, !params.no_timestamps );
247		wparams.setFlag( eFullParamsFlags::PrintSpecial, params.print_special );
248		wparams.setFlag( eFullParamsFlags::Translate, params.translate );
249		// When there're multiple input files, assuming they're independent clips
250		wparams.setFlag( eFullParamsFlags::NoContext );
251		wparams.language = Whisper::makeLanguageKey( params.language.c_str() );
252		wparams.cpuThreads = params.n_threads;
253		if( params.max_context != UINT_MAX )
254			wparams.n_max_text_ctx = params.max_context;
255		wparams.offset_ms = params.offset_t_ms;
256		wparams.duration_ms = params.duration_ms;
257
258		wparams.setFlag( eFullParamsFlags::TokenTimestamps, params.output_wts || params.max_len > 0 );
259		wparams.thold_pt = params.word_thold;
260		wparams.max_len = params.output_wts && params.max_len == 0 ? 60 : params.max_len;
261
262		wparams.setFlag( eFullParamsFlags::SpeedupAudio, params.speed_up );
263
264		// This callback is called on each new segment
265		if( !wparams.flag( eFullParamsFlags::PrintRealtime ) )
266		{
267			wparams.new_segment_callback = &newSegmentCallback;
268			wparams.new_segment_callback_user_data = &params;
269		}
270
271		// example for abort mechanism
272		// in this example, we do not abort the processing, but we could if the flag is set to true
273		// the callback is called before every encoder run - if it returns false, the processing is aborted
274		std::atomic_bool is_aborted = false;
275		{
276			wparams.encoder_begin_callback = &beginSegmentCallback;
277			wparams.encoder_begin_callback_user_data = &is_aborted;
278		}
279
280		if( STREAM_AUDIO && !wparams.flag( eFullParamsFlags::TokenTimestamps ) )
281		{
282			ComLight::CComPtr<iAudioReader> reader;
283			CHECK( mf->openAudioFile( fname.c_str(), params.diarize, &reader ) );
284			sProgressSink progressSink{ nullptr, nullptr };
285			hr = context->runStreamed( wparams, progressSink, reader );
286		}
287		else
288		{
289			// Token-level timestamps feature is not currently implemented when streaming the audio
290			// When these timestamps are requested, fall back to buffered mode.
291			ComLight::CComPtr<iAudioBuffer> buffer;
292			CHECK( mf->loadAudioFile( fname.c_str(), params.diarize, &buffer ) );
293			hr = context->runFull( wparams, buffer );
294		}
295
296		if( FAILED( hr ) )
297		{
298			printError( "Unable to process audio", hr );
299			return 10;
300		}
301
302		if( params.output_txt )
303		{
304			bool timestamps = !params.no_timestamps;
305			hr = writeText( context, fname.c_str(), timestamps );
306			if( FAILED( hr ) )
307				printError( "Unable to produce the text file", hr );
308		}
309
310		if( params.output_srt )
311		{
312			hr = writeSubRip( context, fname.c_str() );
313			if( FAILED( hr ) )
314				printError( "Unable to produce the text file", hr );
315		}
316
317		if( params.output_vtt )
318		{
319			hr = writeWebVTT( context, fname.c_str() );
320			if( FAILED( hr ) )
321				printError( "Unable to produce the text file", hr );
322		}
323	}
324
325	context->timingsPrint();
326	context = nullptr;
327	return 0;
328}