/* Настоящие media_async/uasync/Opus;управляемый backend проверяет retry,busy,изоляцию и освобождение после join. */ #define _GNU_SOURCE #include "../src/chat/chat_whisper.h" #include "../src/chat/chat_core_priv.h" #include "../src/chat/chat_setting.h" #include "../src/utun_instance.h" #include "../src/media_async/media_async.h" #include "../src/media_async/voice_file.h" #include "../lib/u_async.h" #include "../lib/mem.h" #include "../lib/platform_compat.h" #include #include #include #include #include #include #include struct whisper_context { unsigned id; }; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t changed = PTHREAD_COND_INITIALIZER; static unsigned loads, frees, alive, entered; static int fail_load, block, fail_full, sample_count, no_segments; static uint8_t reply[32] = {42}; static unsigned callbacks; static const char* fail_site; static unsigned fail_bytes; static char directory[512], wav[600], opus[600]; static struct UASYNC* ua; extern void* __real_u_calloc_impl(uint32_t, uint32_t, const char*); void* __wrap_u_calloc_impl(uint32_t count, uint32_t bytes, const char* site) { if (fail_site && strstr(site, fail_site)) { fail_site = NULL; DEBUG_WARN(DEBUG_CATEGORY_CHAT, "whisper test: allocation refused at %s", site); return NULL; } return __real_u_calloc_impl(count, bytes, site); } extern void* __real_u_malloc_impl(uint32_t, const char*); void* __wrap_u_malloc_impl(uint32_t bytes, const char* site) { if (fail_bytes == bytes && strstr(site, "chat_whisper.c")) { fail_bytes = 0; DEBUG_WARN(DEBUG_CATEGORY_CHAT, "whisper test: text allocation refused at %s", site); return NULL; } return __real_u_malloc_impl(bytes, site); } struct whisper_context_params whisper_context_default_params(void) { return (struct whisper_context_params){0}; } struct whisper_full_params whisper_full_default_params(int strategy) { (void)strategy; return (struct whisper_full_params){0}; } struct whisper_context* whisper_init_from_file_with_params(const char* path, struct whisper_context_params params) { assert(!params.use_gpu && *path); pthread_mutex_lock(&mutex); loads++; if (fail_load) { fail_load--; pthread_mutex_unlock(&mutex); return NULL; } struct whisper_context* ctx = u_calloc(1, sizeof(*ctx)); assert(ctx); ctx->id = loads; alive++; pthread_mutex_unlock(&mutex); return ctx; } void whisper_free(struct whisper_context* ctx) { assert(ctx); pthread_mutex_lock(&mutex); assert(alive); alive--; frees++; pthread_mutex_unlock(&mutex); u_free(ctx); } int whisper_full(struct whisper_context* ctx, struct whisper_full_params params, const float* samples, int count) { assert(ctx && !strcmp(params.language, "ru") && count > 0); for (int i = 0; i < count; i++) { assert(isfinite(samples[i]) && fabsf(samples[i]) <= 1.0f); if (count == 4000) assert(fabsf(samples[i] - .25f) < .0001f); } pthread_mutex_lock(&mutex); sample_count = count; entered++; pthread_cond_broadcast(&changed); while (block) pthread_cond_wait(&changed, &mutex); int rc = fail_full; pthread_mutex_unlock(&mutex); return rc; } int whisper_full_n_segments(struct whisper_context* ctx) { assert(ctx); return no_segments ? 0 : 2; } const char* whisper_full_get_segment_text(struct whisper_context* ctx, int i) { assert(ctx); return i ? " world" : "hello"; } struct result { unsigned count; int error; }; static void completed(void* arg, const char* channel, const char* text, int error, const uint8_t id[32]) { struct result* result = arg; assert(channel && id && !memcmp(id, reply, 32)); assert(error ? !text : text && !strcmp(text, no_segments ? "" : "hello world")); result->count++; result->error = error; callbacks++; } static void await(struct result* result, unsigned count) { for (int i = 0; result->count < count && i < 1000; i++) uasync_poll(ua, 10); assert(result->count == count); } static void submit(struct UTUN_INSTANCE* inst, const char* file, struct result* result) { chat_whisper_transcribe(inst, file, "1", reply, completed, result); } static void instance(struct UTUN_INSTANCE* inst, struct chat_core_ctx* cc) { memset(inst, 0, sizeof(*inst)); memset(cc, 0, sizeof(*cc)); inst->chat_core = cc; cc->inst = inst; cc->initialized = 1; inst->chat_started = 1; inst->ua = ua; inst->media_async = media_async_create(); assert(inst->media_async); assert(!chat_setting_set(inst, "whisper_enabled", "1")); assert(!chat_setting_set(inst, "whisper_model_path", "test-model")); } static void stop(struct UTUN_INSTANCE* inst) { inst->chat_started = 0; chat_whisper_destroy(inst); media_async_destroy(inst->media_async); inst->media_async = NULL; assert(!inst->chat_core->whisper); } static void fixture(void) { snprintf(directory, sizeof(directory), "/tmp/utun-whisper-XXXXXX"); assert(mkdtemp(directory)); snprintf(wav, sizeof(wav), "%s/input.wav", directory); snprintf(opus, sizeof(opus), "%s/input.opus", directory); FILE* file = fopen(wav, "wb"); assert(file); uint8_t header[44] = {'R','I','F','F',0xa4,0x3e,0,0,'W','A','V','E','f','m','t',' ',16,0,0,0,1,0,2,0, 0x80,0x3e,0,0,0,0xfa,0,0,4,0,16,0,'d','a','t','a',0x80,0x3e,0,0}; /* 4000 стереокадров 16kHz:0.25сек,после downmix постоянный0.25. */ assert(fwrite(header, 1, sizeof(header), file) == sizeof(header)); for (int i = 0; i < 4000; i++) { const uint8_t frame[] = {0,0x10,0,0x30}; assert(fwrite(frame, 1, 4, file) == 4); } assert(!fclose(file)); int16_t* samples = u_malloc(48000 * sizeof(*samples)); assert(samples); for (int i = 0; i < 24000; i++) { samples[i * 2] = (int16_t)(sin(i * .03) * 1000); samples[i * 2 + 1] = samples[i * 2]; } struct attachment_info info = {0}; assert(!voice_file_encode(samples, 48000, 48000, 2, 1, opus, &info)); u_free(samples); } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_CHAT, DEBUG_LEVEL_DEBUG); size_t baseline = u_get_allocated_count(); ua = uasync_create(); assert(ua); fixture(); struct UTUN_INSTANCE a, b; struct chat_core_ctx ca, cb; struct result one = {0}, two = {0}, busy = {0}; instance(&a, &ca); instance(&b, &cb); fail_load = 1; submit(&a, wav, &one); await(&one, 1); assert(one.error == -1 && !alive); submit(&a, wav, &one); await(&one, 2); assert(!one.error && sample_count == 4000 && alive == 1 && loads == 2); submit(&a, opus, &one); await(&one, 3); assert(!one.error && sample_count == 8000 && loads == 2); assert(!chat_setting_set(&a, "whisper_model_path", "replacement")); fail_load = 1; submit(&a, wav, &one); await(&one, 4); assert(one.error == -1 && alive == 1); submit(&a, wav, &one); await(&one, 5); assert(!one.error && alive == 1 && loads == 4 && frees == 1); block = 1; unsigned previous = entered; submit(&a, wav, &one); submit(&a, opus, &busy); assert(busy.count == 1 && busy.error == CHAT_WHISPER_BUSY); submit(&b, opus, &two); pthread_mutex_lock(&mutex); struct timespec deadline; assert(!clock_gettime(CLOCK_REALTIME, &deadline)); deadline.tv_sec += 10; while (entered < previous + 2) assert(pthread_cond_timedwait(&changed, &mutex, &deadline) != ETIMEDOUT); assert(alive == 2); block = 0; pthread_cond_broadcast(&changed); pthread_mutex_unlock(&mutex); await(&one, 6); await(&two, 1); assert(!one.error && !two.error); no_segments = 1; submit(&a, wav, &one); await(&one, 7); assert(!one.error); no_segments = 0; fail_full = 1; submit(&a, wav, &one); await(&one, 8); assert(one.error == -1); fail_full = 0; /* Синхронный отказ media_async освобождает job/input и снимает busy для повтора. */ fail_site = "media_async.c"; submit(&a, wav, &one); assert(!fail_site && one.count == 9 && one.error == -1); /* Отсоединение busy-владельца не освобождает модель до join и даёт CANCELLED. */ previous = callbacks; submit(&a, wav, &one); a.chat_started = 0; chat_whisper_destroy(&a); assert(!ca.whisper && callbacks == previous); media_async_destroy(a.media_async); a.media_async = NULL; assert(one.count == 10 && one.error == MEDIA_ASYNC_CANCELLED); stop(&b); assert(!alive && frees == loads - 2); /* Новый сервис не наследует ready/closing старого владельца. */ a.chat_started = 1; a.media_async = media_async_create(); submit(&a, opus, &one); await(&one, 11); assert(!one.error); stop(&a); instance(&a, &ca); struct result extra = {0}; fail_site = "chat_whisper.c"; submit(&a, wav, &extra); assert(!fail_site && extra.count == 1 && extra.error == -1); fail_bytes = 12; submit(&a, wav, &extra); await(&extra, 2); assert(!fail_bytes && extra.error == -1); submit(&a, wav, &extra); await(&extra, 3); assert(!extra.error); /* Штатный writer и reader согласованы для всех поддерживаемых частот/каналов. */ const int rates[] = {8000,12000,16000,24000,48000}; for (unsigned r = 0; r < 5; r++) for (int channels = 1; channels <= 2; channels++) { size_t count = (size_t)rates[r] / 2 * channels; int16_t* samples = u_calloc((uint32_t)count, sizeof(*samples)); assert(samples); struct attachment_info info = {0}; assert(!voice_file_encode(samples, count, rates[r], channels, 1, opus, &info)); u_free(samples); unsigned expected = extra.count + 1; submit(&a, opus, &extra); await(&extra, expected); assert(!extra.error && sample_count == 8000); } stop(&a); assert(!remove(wav) && !remove(opus) && !rmdir(directory)); uasync_poll(ua, 0); uasync_destroy(ua, 0); assert(!alive && u_get_allocated_count() == baseline); DEBUG_INFO(DEBUG_CATEGORY_CHAT, "PASS whisper:retry/cache/replace,busy,two instances,empty/error,cancel/restart,PCM/WAV/real Opus;callbacks=%u", callbacks); return 0; }