Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/deepfist-cw-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ the UI thread; stopping joins the current inference before destroying state.

The single DeepFist choice enables activity normalization, completed-mark
admission and bounded pending-character carry. These are developer settings;
there is no second normalized decoder in the UI. Output has no calibrated
there is no second normalized decoder in the UI. It also lowers the
activity-gate threshold from the stream default 12 to 3 so weak off-air CW is
not gated out (#5950); that value is what every user of this backend runs.
Output has no calibrated
ggmorse cost and is displayed without inventing confidence, pitch or speed.
DeepFist output does not feed automatic callsign spotting. Other monitored
slices and speaker gain/mute do not alter the selected decoder input. This is
Expand Down
2 changes: 1 addition & 1 deletion src/core/deepfist/DeepFistStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace AetherSDR {
// One worker owns this fixed-memory 3.2 kHz streaming context.
class DeepFistStream {
public:
// Explicit developer replay configuration; the application uses these defaults.
// Stream defaults for developer replay; the app overrides some in DeepFistCwModel::appParameters().
struct Parameters {
int tickSamples = 1280;
double guardSeconds = 1.3;
Expand Down
16 changes: 13 additions & 3 deletions src/models/DeepFistCwModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,26 @@ bool matches(const QString& path, qint64 size, const QByteArray& digest)
return hash.addData(&file) && hash.result().toHex() == digest;
}
}
DeepFistStream::Parameters DeepFistCwModel::appParameters()
{
DeepFistStream::Parameters parameters;
parameters.requireCompletedMark = true;
parameters.carryPending = true;
parameters.normalizeActivity = true;
// The stream default (12) sits between dead air and a tuned-in signal (n9bc/DeepFist
// tools/squelch.py); weak off-air CW scores in that gap and was gated out (#5950).
// Steady carriers are held off by the completed-mark guard above, not by this value.
parameters.activityThreshold = 3.f;
Comment thread
NF0T marked this conversation as resolved.
Comment thread
NF0T marked this conversation as resolved.
return parameters;
}
DeepFistCwModel::DeepFistCwModel(QObject* parent)
: DeepFistCwModel(modelDirectory(), DeepFistModelAssets::releaseBaseUrl(), nullptr, parent) {}
DeepFistCwModel::DeepFistCwModel(QString directory, QString baseUrl,
QNetworkAccessManager* network, QObject* parent)
: QObject(parent), m_assets(std::make_unique<DeepFistModelAssets>(directory, baseUrl,
DeepFistModelAssets::manifest(), network)), m_directory(std::move(directory))
{
m_parameters.requireCompletedMark = true;
m_parameters.carryPending = true;
m_parameters.normalizeActivity = true;
m_parameters = appParameters();
connect(m_assets.get(), &DeepFistModelAssets::checking, this, [this] {
setStatus(tr("Checking model…"));
});
Expand Down
2 changes: 2 additions & 0 deletions src/models/DeepFistCwModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class DeepFistCwModel final : public QObject {
bool isRunning() const { return m_running; }
QString status() const { return m_status; }
static QString modelDirectory();
// The parameters the application runs DeepFist with (the default constructors use them).
static DeepFistStream::Parameters appParameters();
signals:
void textDecoded(const QString& text);
void statusChanged(const QString& status);
Expand Down
94 changes: 91 additions & 3 deletions tests/deepfist_cw_model_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <QStringList>
#include <cstdio>
#include <limits>
#include <random>

using namespace AetherSDR;
namespace {
Expand Down Expand Up @@ -232,9 +233,92 @@ bool carrierRegression(const QByteArray& directory)
if (stream.failed()) { return false; }
}
}
std::fprintf(stderr, "carrier guard disabled: '%s'; enabled: '%s'\n",
qPrintable(outputs[0].simplified()), qPrintable(outputs[1].simplified()));
return outputs[0].simplified() == "9" && outputs[1].simplified().isEmpty();
// The app's parameters hold the carrier off too. The completed-mark guard does that at any
// activityThreshold (a carrier has no key-up), so this arm says nothing about the value 3.
QString appOutput;
{
DeepFistStream stream(DeepFistCwModel::appParameters());
Resampler resampler(24000, 3200, 4096);
for (int offset = 0; offset < 240000; offset += 240) {
float audio[240];
for (int i = 0; i < 240; ++i) {
audio[i] = 0.3f * std::sin(2.0 * 3.141592653589793 * 680.0 * (offset+i) / 24000.0);
}
const QByteArray bytes = resampler.process(audio, 240);
std::vector<float> mono(bytes.size()/4);
std::memcpy(mono.data(), bytes.constData(), bytes.size());
appOutput += stream.process(mono.data(), mono.size(), model);
if (stream.failed()) { return false; }
}
}
std::fprintf(stderr, "carrier guard disabled: '%s'; enabled: '%s'; app parameters: '%s'\n",
qPrintable(outputs[0].simplified()), qPrintable(outputs[1].simplified()),
qPrintable(appOutput.simplified()));
return outputs[0].simplified() == "9" && outputs[1].simplified().isEmpty()
&& appOutput.simplified().isEmpty();
}
// #5950: off-air CW whose keying ratio falls between dead air and a strong signal was gated
// out at activityThreshold 12. CONSTRUCTED fixture: the CQ TEST CQ tone plus in-band noise
// (40 fixed random-phase tones, 550-650 Hz, seed 5950) — it exercises the gate only and does
// not stand for any measured signal; the field evidence is the recordings attached to #5950.
// keyed = false leaves the noise alone.
QString weakSignalDecode(lyra::dsp::DeepFistModel& model, DeepFistStream::Parameters parameters,
bool keyed = true)
{
constexpr int rate = 3200;
// In-band noise RMS: from 0.06 to 0.13 the final CQ decodes only with the app's parameters
// (0.05: both thresholds decode it all); 0.09 sits in the middle.
constexpr double noiseRms = 0.09;
const QVector<float> clean = testAudio(rate);
// std::mt19937's output sequence is fixed by the standard; the distribution classes are
// not, so the uniform values come straight from the engine to keep this fixture identical
// on every standard library.
std::mt19937 generator(5950);
const auto unit = [&generator] { return static_cast<double>(generator()) / 4294967296.0; };
double frequency[40];
double phase[40];
for (int k = 0; k < 40; ++k) {
frequency[k] = 550.0 + 100.0 * unit();
phase[k] = 2.0 * 3.141592653589793 * unit();
}
std::vector<float> audio(clean.size());
for (qsizetype i = 0; i < clean.size(); ++i) {
double noise = 0.0;
for (int k = 0; k < 40; ++k) {
noise += std::sin(2.0 * 3.141592653589793 * frequency[k] * i / rate + phase[k]);
}
audio[i] = static_cast<float>((keyed ? clean[i] : 0.f) + noiseRms * noise / std::sqrt(20.0));
}
DeepFistStream stream(parameters);
QString text;
for (size_t offset = 0; offset < audio.size(); offset += 320) {
const int count = static_cast<int>(std::min<size_t>(320, audio.size() - offset));
text += stream.process(audio.data() + offset, count, model);
if (stream.failed()) { return QStringLiteral("<failed>"); }
}
return text.simplified();
}
bool weakSignal(const QByteArray& directory)
{
lyra::dsp::DeepFistModel model;
if (!model.load(directory.toStdString())) { return false; }
const QString app = weakSignalDecode(model, DeepFistCwModel::appParameters());
DeepFistStream::Parameters strict = DeepFistCwModel::appParameters();
strict.activityThreshold = 12.f;
const QString previous = weakSignalDecode(model, strict);
std::fprintf(stderr, "weak signal: app parameters '%s'; threshold 12 '%s'\n",
qPrintable(app), qPrintable(previous));
// Noise alone must not print more with the app's parameters than at 12.
const QString appNoise = weakSignalDecode(model, DeepFistCwModel::appParameters(), false);
const QString previousNoise = weakSignalDecode(model, strict, false);
std::fprintf(stderr, "noise only: app parameters '%s'; threshold 12 '%s'\n",
qPrintable(appNoise), qPrintable(previousNoise));
const QString failed = QStringLiteral("<failed>");
if (previous == failed || appNoise == failed || previousNoise == failed) { return false; }
// The gate effect: the final CQ survives only with the app's parameters. The trailing
// noise-only tail dilutes the 6 s window's keying ratio, which is what closes the gate at 12.
return app.endsWith(QStringLiteral(" CQ")) && !previous.endsWith(QStringLiteral("CQ"))
&& appNoise.size() <= previousNoise.size();
}
bool churn()
{
Expand Down Expand Up @@ -337,6 +421,10 @@ int main(int argc, char** argv)
if (directory.isEmpty()) { return 77; }
return carrierRegression(directory) ? 0 : 1;
}
if (argc > 1 && QString::fromLocal8Bit(argv[1]) == "--weak") {
if (directory.isEmpty()) { return 77; }
return weakSignal(directory) ? 0 : 1;
}
if (argc > 1 && QString::fromLocal8Bit(argv[1]) == "--churn") {
if (directory.isEmpty()) { return 77; }
return churn() ? 0 : 1;
Expand Down
3 changes: 2 additions & 1 deletion tests/tests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -6944,7 +6944,8 @@ if(ENABLE_DEEPFIST_EXPERIMENT)
add_test(NAME deepfist_cw_model_test COMMAND deepfist_cw_model_test)
add_test(NAME deepfist_carrier_regression_test COMMAND deepfist_cw_model_test --carrier)
add_test(NAME deepfist_cw_churn_test COMMAND deepfist_cw_model_test --churn)
set_tests_properties(deepfist_cw_churn_test deepfist_carrier_regression_test PROPERTIES
add_test(NAME deepfist_weak_signal_test COMMAND deepfist_cw_model_test --weak)
set_tests_properties(deepfist_cw_churn_test deepfist_carrier_regression_test deepfist_weak_signal_test PROPERTIES
SKIP_RETURN_CODE 77 TIMEOUT 120)
add_test(NAME deepfist_cw_model_inference_test COMMAND deepfist_cw_model_test --infer)
add_test(NAME deepfist_cw_model_download_inference_test COMMAND deepfist_cw_model_test --download-infer)
Expand Down
Loading