feat: v0.1.0 — client Sailfish per hermes-webui con voce
- ApiClient HTTP/SSE: login, sessioni, chat/start, stream SSE, transcribe, tts - ChatModel: streaming live + risincronizzazione dallo stato del server - CookieJar persistente (0600, mai la password); Settings - Recorder OGG/Opus (fallback WAV/PCM, retry); Player TTS (QMediaPlayer) - UI Silica: splash, login, sessioni, chat (dettatura + read-aloud), settings, cover - Sailjail Internet;Audio;Microphone; icone; spec RPM; docs PROTOCOL/BUILD - tests/core_test: unit parser SSE + integrazione live (probe :8899)
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// Test locale del core di harbour-hermes (compilato con Qt desktop).
|
||||
//
|
||||
// Uso:
|
||||
// ./core-test -> solo unit test del parser SSE
|
||||
// ./core-test --live -> integrazione completa contro l'istanza di prova
|
||||
// del WebUI su http://127.0.0.1:8899
|
||||
// (login -> nuova sessione -> turno -> TTS -> STT)
|
||||
//
|
||||
// Il test live legge la password di prova da /tmp/hermes-probe/pw e usa
|
||||
// l'audio di prova /tmp/hermes-probe/out/prova.ogg.
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QTimer>
|
||||
|
||||
#include "apiclient.h"
|
||||
#include "chatmodel.h"
|
||||
#include "settings.h"
|
||||
#include "sseparser.h"
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
#define CHECK(cond, msg) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
qWarning() << "FAIL:" << msg; \
|
||||
++failures; \
|
||||
} else { \
|
||||
qDebug() << "ok :" << msg; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static void testParser()
|
||||
{
|
||||
qDebug() << "== unit: parser SSE ==";
|
||||
const QByteArray sample =
|
||||
"id: s1:1\n"
|
||||
"event: context_status\n"
|
||||
"data: {\"a\": 1}\n"
|
||||
"\n"
|
||||
": heartbeat\n"
|
||||
"\n"
|
||||
"id: s1:2\n"
|
||||
"event: token\n"
|
||||
"data: {\"text\": \"p\"}\n"
|
||||
"\n";
|
||||
SseParser p;
|
||||
const QList<SseParser::Event> evs = p.feed(sample);
|
||||
CHECK(evs.size() == 2, "due eventi, keepalive ignorato");
|
||||
if (evs.size() == 2) {
|
||||
CHECK(evs.at(0).name == QLatin1String("context_status"), "primo evento context_status");
|
||||
CHECK(evs.at(0).id == QLatin1String("s1:1"), "id del primo evento");
|
||||
CHECK(evs.at(1).name == QLatin1String("token"), "secondo evento token");
|
||||
CHECK(evs.at(1).data == QLatin1String("{\"text\": \"p\"}"), "data del token");
|
||||
}
|
||||
|
||||
// Chunk spezzati a meta' evento (caso reale della rete).
|
||||
SseParser p2;
|
||||
CHECK(p2.feed("event: token\ndata: {\"te").isEmpty(), "chunk parziale: nessun evento");
|
||||
const QList<SseParser::Event> evs2 = p2.feed("xt\": \"ok\"}\n\n");
|
||||
CHECK(evs2.size() == 1, "evento completato dal secondo chunk");
|
||||
if (!evs2.isEmpty())
|
||||
CHECK(evs2.at(0).data.contains(QLatin1String("ok")), "payload ricomposto");
|
||||
|
||||
// Evento con data su piu' righe.
|
||||
SseParser p3;
|
||||
const QList<SseParser::Event> evs3 = p3.feed("event: x\ndata: a\ndata: b\n\n");
|
||||
CHECK(evs3.size() == 1 && evs3.at(0).data == QLatin1String("a\nb"),
|
||||
"data multi-riga unito con newline");
|
||||
}
|
||||
|
||||
static QString readFileTrim(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
return QString();
|
||||
return QString::fromUtf8(f.readAll()).trimmed();
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
app.setOrganizationName(QStringLiteral("harbour-test"));
|
||||
app.setApplicationName(QStringLiteral("hermes-test"));
|
||||
|
||||
testParser();
|
||||
|
||||
if (!app.arguments().contains(QStringLiteral("--live"))) {
|
||||
qDebug() << (failures == 0 ? "UNIT: tutti i test passati" : "UNIT: FALLIMENTI");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
qDebug() << "== integrazione live (probe :8899) ==";
|
||||
|
||||
QTimer::singleShot(180000, &app, []() {
|
||||
qWarning() << "TIMEOUT globale del test live";
|
||||
qApp->exit(2);
|
||||
});
|
||||
|
||||
Settings settings;
|
||||
settings.setBaseUrl(QStringLiteral("http://127.0.0.1:8899"));
|
||||
ApiClient api(&settings);
|
||||
ChatModel chat(&api);
|
||||
|
||||
const QString pw = readFileTrim(QStringLiteral("/tmp/hermes-probe/pw"));
|
||||
if (pw.isEmpty()) {
|
||||
qWarning() << "password del probe mancante in /tmp/hermes-probe/pw";
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool chatSent = false;
|
||||
bool chatDone = false;
|
||||
bool ttsDone = false;
|
||||
bool sttDone = false;
|
||||
|
||||
QObject::connect(&api, &ApiClient::loggedInChanged, [&]() {
|
||||
if (api.loggedIn()) {
|
||||
qDebug() << "LIVE: login ok";
|
||||
chat.newSession();
|
||||
}
|
||||
});
|
||||
QObject::connect(&api, &ApiClient::sessionOpened, [&]() {
|
||||
if (chatSent)
|
||||
return;
|
||||
chatSent = true;
|
||||
qDebug() << "LIVE: sessione" << chat.sessionId();
|
||||
chat.send(QStringLiteral("Rispondi solo con la parola pong. Non usare strumenti."));
|
||||
});
|
||||
QObject::connect(&chat, &ChatModel::assistantFinished, [&]() {
|
||||
qDebug() << "LIVE: risposta =" << chat.lastAssistantText();
|
||||
chatDone = chat.lastAssistantText().contains(QLatin1String("pong"));
|
||||
api.speak(QStringLiteral("Questa e' la voce italiana del client Hermes per Sailfish."));
|
||||
});
|
||||
QObject::connect(&api, &ApiClient::ttsReady, [&](const QString &path) {
|
||||
QFile f(path);
|
||||
ttsDone = f.size() > 1000;
|
||||
qDebug() << "LIVE: ttsReady" << path << "bytes:" << f.size();
|
||||
api.transcribeFile(QStringLiteral("/tmp/hermes-probe/out/prova.ogg"));
|
||||
});
|
||||
QObject::connect(&api, &ApiClient::ttsFailed, [&](const QString &err) {
|
||||
qWarning() << "LIVE: ttsFailed" << err;
|
||||
api.transcribeFile(QStringLiteral("/tmp/hermes-probe/out/prova.ogg"));
|
||||
});
|
||||
QObject::connect(&api, &ApiClient::transcriptionReady, [&](const QString &text) {
|
||||
qDebug() << "LIVE: trascrizione =" << text;
|
||||
sttDone = !text.isEmpty();
|
||||
QTimer::singleShot(200, qApp, [&]() {
|
||||
const bool pass = chatDone && ttsDone && sttDone;
|
||||
qDebug() << (pass ? "LIVE: TUTTI I PASSAGGI OK" : "LIVE: qualche passaggio e' fallito");
|
||||
qApp->exit(pass ? 0 : 1);
|
||||
});
|
||||
});
|
||||
QObject::connect(&api, &ApiClient::transcriptionFailed, [&](const QString &err) {
|
||||
qWarning() << "LIVE: transcriptionFailed" << err;
|
||||
qApp->exit(1);
|
||||
});
|
||||
QObject::connect(&api, &ApiClient::chatStartFailed, [&](const QString &err) {
|
||||
qWarning() << "LIVE: chatStartFailed" << err;
|
||||
qApp->exit(1);
|
||||
});
|
||||
|
||||
api.login(pw);
|
||||
qDebug() << "LIVE: login inviato";
|
||||
return app.exec();
|
||||
}
|
||||
Reference in New Issue
Block a user