spot-convert.c¶
This is the source code for the spot-convert command-line tool.
Instructions¶
See spot-convert.
Code¶
Available in this TrulyNatural SDK installation at ~/Sensory/TrulyNaturalSDK/7.6.1/sample/c/src/spot-convert.c
spot-convert.c
/* Sensory Confidential
* Copyright (C)2016-2025 Sensory, Inc. https://sensory.com/
*
* TrulyHandsfree SDK model conversion command-line utility.
*------------------------------------------------------------------------------
*/
#include <snsr.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define EMBED_TASK_VERSION "~0.6.0 || 1.0.0"
#define HEADER_NAME "-search.h"
#define SEARCH_NAME "-search.bin"
#define ACMODEL_NAME "-net.bin"
#define FILENAME_SIZE 1023
#define KEY_SIZE 64
#define TARGET_SIZE 16
#if defined(_MSC_VER) && (_MSC_VER < 1900)
# define snprintf _snprintf
#endif
typedef struct {
const char *basename; /* output file prefix */
const char *slot; /* slot prefix */
const char *target; /* embedded target descriptor */
int fileNameInfo; /* append version and operating point to filename */
int outputC; /* true to generate C output files */
int point; /* operating point to convert */
int verbose; /* feedback verbosity */
} ConvertContext;
static void
fatal(int rc, const char *format, ...)
{
va_list a;
fprintf(stderr, "ERROR: ");
va_start(a, format);
vfprintf(stderr, format, a);
va_end(a);
fprintf(stderr, "\n");
exit(rc);
}
/* Concatenates head, ".", and tail into key, and returns
* a pointer to key.
*/
const char *
slotKey(const char *head, const char *tail, char key[KEY_SIZE + 1])
{
if (strlen(head) + strlen(tail) + 2 > KEY_SIZE)
fatal(SNSR_RC_INVALID_ARG, "-q slotname prefix is too long.");
strncpy(key, head, KEY_SIZE);
strncat(key, ".", KEY_SIZE);
strncat(key, tail, KEY_SIZE);
key[KEY_SIZE - 1] = '\0';
return key + (*head == '\0'); /* skip leading . if head is empty */
}
static void
writeFile(SnsrStream model, ConvertContext *ctx, const char *tag,
const char *pre, const char *ver, const char *prod, const char *mid,
const char *post, const char *ext, const char *mode)
{
SnsrRC r;
SnsrStream output;
size_t written;
char filename[FILENAME_SIZE + 1];
filename[FILENAME_SIZE] = '\0';
strncpy(filename, pre, FILENAME_SIZE);
if (ctx->fileNameInfo) {
if (ver) {
strncat(filename, ver, FILENAME_SIZE);
strncat(filename, "-", FILENAME_SIZE);
}
strncat(filename, mid, FILENAME_SIZE);
strncat(filename, prod, FILENAME_SIZE);
}
strncat(filename, post, FILENAME_SIZE);
strncat(filename, ext, FILENAME_SIZE);
written = snsrStreamGetMeta(model, SNSR_ST_META_BYTES_WRITTEN);
output = snsrStreamFromFileName(filename, mode);
snsrRetain(output);
snsrStreamCopy(output, model, written);
r = snsrStreamRC(output);
if (r != SNSR_RC_OK) fatal(r, snsrStreamErrorDetail(output));
snsrRelease(output);
if (ctx->verbose > 0) {
printf("wrote %s to \"%s\"\n", tag, filename);
fflush(stdout);
}
}
static SnsrRC
writeEmbeddedFiles(SnsrSession s, ConvertContext *ctx, const char *slot)
{
SnsrRC r;
SnsrStream net = NULL, sch = NULL, hdr = NULL;
#define OP_SIZE 6
char op[OP_SIZE];
char kb[KEY_SIZE + 1];
char srcTarget[TARGET_SIZE + 1];
const char *tSliceVersion = NULL;
int prodReady = 0;
const char *key, *prod = "prod-";
r = snsrSetString(s, slotKey(slot, SNSR_EMBEDDED_TARGET, kb), ctx->target);
if (r == SNSR_RC_SETTING_NOT_FOUND)
fatal(snsrRC(s), "This model cannot be converted to DSP format."
" (%s)", snsrErrorDetail(s));
snsrGetStream(s, slotKey(slot, SNSR_EMBEDDED_ACMODEL_STREAM, kb), &net);
if (net) snsrRetain(net);
snsrGetStream(s, slotKey(slot, SNSR_EMBEDDED_SEARCH_STREAM, kb), &sch);
if (sch) snsrRetain(sch);
snsrGetStream(s, slotKey(slot, SNSR_EMBEDDED_HEADER_STREAM, kb), &hdr);
if (hdr) snsrRetain(hdr);
key = slotKey(slot, SNSR_RES_MIN_EMBEDDED_VERSION, kb);
snsrGetString(s, key, &tSliceVersion);
if (tSliceVersion) snsrRetain(tSliceVersion);
key = slotKey(slot, SNSR_RES_EMBEDDED_MODEL_PRODUCTION_READY, kb);
r = snsrGetInt(s, key, &prodReady);
if (r != SNSR_RC_OK) fatal(snsrRC(s), "%s", snsrErrorDetail(s));
snprintf(op, OP_SIZE, "op%02i-", ctx->point);
if (ctx->verbose > 0) {
printf("operating-point: %i\n", ctx->point);
fflush(stdout);
}
if (!prodReady) prod = "dev-";
if (ctx->verbose > 0) {
printf("production-ready: %s\n", prodReady? "yes": "no");
fflush(stdout);
}
writeFile(net, ctx, "acoustic model (bin)",
ctx->basename, tSliceVersion, prod, op, "net", ".bin", "w");
snsrRelease(net);
writeFile(sch, ctx, "search model (bin)",
ctx->basename, tSliceVersion, prod, op, "search", ".bin", "w");
snsrRelease(sch);
writeFile(hdr, ctx, "search header",
ctx->basename, tSliceVersion, prod, op, "search", ".h", "wt");
snsrRelease(hdr);
if (ctx->outputC) {
memset(srcTarget, 0, TARGET_SIZE + 1);
strncpy(srcTarget, "src:", TARGET_SIZE);
strncat(srcTarget, ctx->target, TARGET_SIZE);
snsrSetString(s, slotKey(slot, SNSR_EMBEDDED_TARGET, kb), srcTarget);
snsrGetStream(s, slotKey(slot, SNSR_EMBEDDED_ACMODEL_STREAM, kb), &net);
writeFile(net, ctx, "acoustic model (C)",
ctx->basename, tSliceVersion, prod, op, "net", ".c", "wt");
snsrGetStream(s, slotKey(slot, SNSR_EMBEDDED_SEARCH_STREAM, kb), &sch);
writeFile(sch, ctx, "search model (C)",
ctx->basename, tSliceVersion, prod, op, "search", ".c", "wt");
}
snsrRelease(tSliceVersion);
return snsrRC(s);
}
static SnsrRC
convertAllPoints(SnsrSession s, const char *key, void *data)
{
ConvertContext *c = (ConvertContext *)data;
const char *slot = c->slot;
char keyBuff[KEY_SIZE + 1];
SnsrRC r;
snsrGetInt(s, slotKey(slot, SNSR_RES_AVAILABLE_POINT, keyBuff), &c->point);
r = snsrSetInt(s, slotKey(slot, SNSR_OPERATING_POINT, keyBuff), c->point);
if (r != SNSR_RC_OK) return r;
return writeEmbeddedFiles(s, c, slot);
}
static const char *usageDetail =
"Output filenames are determined by the model parameters:\n"
" $(prefix) [-] [slot$(slotname)-] $(target)- $(version)-\n"
" op$(operating-point)- {dev,prod}- {net,search}.{bin,c,h}\n"
"where:\n"
" prefix specified by the -p option, or taken from the filename\n"
" of the task if -p isn't used.\n"
" version is the oldest DSP library that can run this model.\n"
" -dev- models are limited in runtime or number of recognition\n"
" events and should not be used in products.\n"
" -prod- models are not limited and ready for production use.\n"
"\n"
"Use the -o option to override this filename pattern to:\n"
" $(prefix)[-]{net,search}.{bin,c,h}\n"
"\n"
"The -o and -a options are mutually exclusive.\n"
"\n"
"Output filenames are constrained to never start with \"-\"\n"
"\n"
"Settings are strings used as keys to query or change task behavior.\n"
"Most frequently used for wake words and command sets is operating-point.\n"
"Refer to the " SNSR_NAME " SDK documentation for a complete list and\n"
"descriptions of all supported settings.\n";
static void
usage(const char *name)
{
SnsrSession s;
const char *libInfo;
fprintf(stderr, "Converts " SNSR_NAME " SDK wake word models to "
"THF Micro format.\n\n");
fprintf(stderr,
"usage: %s -t task [options] target\n"
" options:\n"
" -a : convert all operating-points\n"
" -c : create .c output (in addition to .bin)\n"
" -o output : full prefix for output filenames\n"
" -p output-prefix : prefix for output filenames "
"(default: task-target-)\n"
" -q slotname : model slot prefix\n"
" -s setting=value : override a task setting\n"
" -t task : set a task filename (required)\n"
" -v [-v [-v]] : increase verbosity\n", name);
fprintf(stderr, "\n%s", usageDetail);
snsrNew(&s);
snsrGetString(s, SNSR_LIBRARY_INFO, &libInfo);
fprintf(stderr, "\n%s\n", libInfo);
snsrRelease(s);
exit(199);
}
/* Report model license keys that have imminent expiration dates.
*/
static void
reportExpiringModelLicense(SnsrSession s, const char *modelfile)
{
const char *expWarning = NULL;
snsrGetString(s, SNSR_MODEL_LICENSE_WARNING, &expWarning);
if (expWarning)
fprintf(stderr, "WARNING for model \"%s\": %s.\n", modelfile, expWarning);
}
int
main(int argc, char *argv[])
{
SnsrRC r;
SnsrSession s;
char basename[FILENAME_SIZE + 1];
char keyBuff[KEY_SIZE + 1];
int allPoints = 0, o;
const char *prefix = NULL, *task = NULL;
ConvertContext ctx;
extern char *optarg;
extern int optind;
#ifdef SNSR_USE_SECURITY_CHIP
uint32_t *securityChipComms(uint32_t *in);
snsrConfig(SNSR_CONFIG_SECURITY_CHIP, securityChipComms);
#endif
if (argc == 1) usage(argv[0]);
ctx.basename = basename;
ctx.slot = "";
ctx.target = NULL;
ctx.outputC = 0;
ctx.point = 0;
ctx.fileNameInfo = 1;
ctx.verbose = 0;
r = snsrNew(&s);
if (r != SNSR_RC_OK) fatal(r, s? snsrErrorDetail(s): snsrRCMessage(r));
while ((o = getopt(argc, argv, "aco:p:q:s:t:v?")) >= 0) {
switch (o) {
case 'a':
allPoints = 1;
break;
case 'c':
ctx.outputC = 1;
break;
case 'o':
prefix = optarg;
ctx.fileNameInfo = 0;
break;
case 'p':
prefix = optarg;
break;
case 'q':
ctx.slot = optarg;
break;
case 's':
r = snsrSet(s, optarg);
if (r == SNSR_RC_NO_MODEL)
fatal(r, "Set -t task before -s setting=value");
else if (r != SNSR_RC_OK)
fatal(r, snsrErrorDetail(s));
break;
case 't':
snsrLoad(s, snsrStreamFromFileName(optarg, "r"));
snsrRequire(s, SNSR_TASK_TYPE, SNSR_PHRASESPOT);
r = snsrRequire(s, SNSR_TASK_VERSION, EMBED_TASK_VERSION);
if (r != SNSR_RC_OK) fatal(r, snsrErrorDetail(s));
task = optarg;
reportExpiringModelLicense(s, optarg);
break;
case 'v': ctx.verbose++; break;
case '?':
default: usage(argv[0]);
}
}
if (optind != argc - 1) usage(argv[0]);
ctx.target = argv[optind];
if (allPoints && !ctx.fileNameInfo)
fatal(SNSR_RC_INVALID_ARG, "The -a and -o options are mutually exclusive.");
r = snsrRequire(s, SNSR_TASK_TYPE, SNSR_PHRASESPOT);
if (r == SNSR_RC_NO_MODEL) usage(argv[0]);
/* We'll include the source filename in the header output */
r = snsrSetString(s, SNSR_MODEL_NAME, task);
if (r != SNSR_RC_OK) fatal(r, snsrErrorDetail(s));
/* Output filename prefix buffer */
basename[FILENAME_SIZE] = '\0';
if (prefix) strncpy(basename, prefix, FILENAME_SIZE);
else {
char *e;
assert(task);
if ((e = (char *)strrchr(task, '/')))
strncpy(basename, e + 1, FILENAME_SIZE);
else strncpy(basename, task, FILENAME_SIZE);
if ((e = strrchr(basename, '.'))) *e = '\0';
}
if (*basename) strncat(basename, "-", FILENAME_SIZE);
if (ctx.fileNameInfo) {
char *e;
size_t len;
if (*ctx.slot) {
len = strlen(basename);
snprintf(basename + len, FILENAME_SIZE - len, "slot%s-", ctx.slot);
}
len = strlen(basename);
snprintf(basename + len, FILENAME_SIZE - len, "%s-", ctx.target);
if ((e = strchr(basename + len, ':'))) *e = '_';
}
if (ctx.verbose > 1) {
printf("target: %s\n", ctx.target);
printf("basename: %s\n", ctx.basename);
fflush(stdout);
}
if (allPoints) {
snsrForEach(s, slotKey(ctx.slot, SNSR_OPERATING_POINT_LIST, keyBuff),
snsrCallback(convertAllPoints, NULL, &ctx));
} else {
/* Very old models do not have support for operating points */
if (snsrRC(s) != SNSR_RC_OK) fatal(snsrRC(s), snsrErrorDetail(s));
snsrGetInt(s, slotKey(ctx.slot, SNSR_OPERATING_POINT, keyBuff), &ctx.point);
snsrClearRC(s);
writeEmbeddedFiles(s, &ctx, ctx.slot);
}
if (snsrRC(s) != SNSR_RC_OK) fatal(snsrRC(s), snsrErrorDetail(s));
snsrRelease(s);
snsrTearDown();
return 0;
}