SHA256
add r1 robot project
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
#include "r1_voice/conversation.hh"
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
namespace r1_voice {
|
||||
|
||||
Conversation::Conversation(
|
||||
ConversationConfig config)
|
||||
: config_(std::move(config))
|
||||
, stt_(config_.stt)
|
||||
, llm_(config_.llm)
|
||||
, tts_(config_.tts)
|
||||
{
|
||||
}
|
||||
|
||||
bool Conversation::init(
|
||||
const std::string& network_interface)
|
||||
{
|
||||
return tts_.init(
|
||||
network_interface
|
||||
);
|
||||
}
|
||||
|
||||
void Conversation::run()
|
||||
{
|
||||
std::cout
|
||||
<< "\n"
|
||||
<< "====================================\n"
|
||||
<< " R1 Voice Assistant\n"
|
||||
<< "====================================\n"
|
||||
<< "Speak to R1.\n"
|
||||
<< "Press Ctrl+C to exit.\n"
|
||||
<< '\n';
|
||||
|
||||
tts_.speak(
|
||||
"Ahoj. Jsem připraven."
|
||||
);
|
||||
|
||||
// Give TTS time to finish before listening.
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::seconds(2)
|
||||
);
|
||||
|
||||
while (true) {
|
||||
|
||||
const std::string user =
|
||||
stt_.listen();
|
||||
|
||||
if (user.empty())
|
||||
continue;
|
||||
|
||||
try {
|
||||
|
||||
const std::string response =
|
||||
llm_.chat(user);
|
||||
|
||||
if (response.empty())
|
||||
continue;
|
||||
|
||||
tts_.speak(response);
|
||||
|
||||
/*
|
||||
* Avoid immediately hearing the robot's
|
||||
* own TTS output as the next user utterance.
|
||||
*
|
||||
* Later we can replace this with proper
|
||||
* audio echo suppression / playback state.
|
||||
*/
|
||||
const int estimated_ms =
|
||||
1000 +
|
||||
static_cast<int>(
|
||||
response.size() * 50
|
||||
);
|
||||
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(
|
||||
estimated_ms
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
|
||||
std::cerr
|
||||
<< "Conversation error: "
|
||||
<< e.what()
|
||||
<< '\n';
|
||||
|
||||
tts_.speak(
|
||||
"Promiň, při zpracování odpovědi nastala chyba."
|
||||
);
|
||||
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::seconds(2)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace r1_voice
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
#include "r1_voice/llm.hh"
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace r1_voice {
|
||||
|
||||
namespace {
|
||||
|
||||
size_t writeCallback(
|
||||
void* contents,
|
||||
size_t size,
|
||||
size_t nmemb,
|
||||
void* user)
|
||||
{
|
||||
const size_t total = size * nmemb;
|
||||
|
||||
auto* output =
|
||||
static_cast<std::string*>(user);
|
||||
|
||||
output->append(
|
||||
static_cast<char*>(contents),
|
||||
total
|
||||
);
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LLM::LLM(LlmConfig config)
|
||||
: config_(std::move(config))
|
||||
{
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
}
|
||||
|
||||
std::string LLM::escapeJson(
|
||||
const std::string& text) const
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(text.size() + 16);
|
||||
|
||||
for (const char c : text) {
|
||||
switch (c) {
|
||||
case '"':
|
||||
result += "\\\"";
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
result += "\\\\";
|
||||
break;
|
||||
|
||||
case '\n':
|
||||
result += "\\n";
|
||||
break;
|
||||
|
||||
case '\r':
|
||||
result += "\\r";
|
||||
break;
|
||||
|
||||
case '\t':
|
||||
result += "\\t";
|
||||
break;
|
||||
|
||||
default:
|
||||
result += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string LLM::httpPost(
|
||||
const std::string& body) const
|
||||
{
|
||||
CURL* curl = curl_easy_init();
|
||||
|
||||
if (!curl) {
|
||||
throw std::runtime_error(
|
||||
"curl_easy_init() failed"
|
||||
);
|
||||
}
|
||||
|
||||
std::string response;
|
||||
|
||||
curl_slist* headers = nullptr;
|
||||
|
||||
headers = curl_slist_append(
|
||||
headers,
|
||||
"Content-Type: application/json"
|
||||
);
|
||||
|
||||
curl_easy_setopt(
|
||||
curl,
|
||||
CURLOPT_URL,
|
||||
config_.url.c_str()
|
||||
);
|
||||
|
||||
curl_easy_setopt(
|
||||
curl,
|
||||
CURLOPT_HTTPHEADER,
|
||||
headers
|
||||
);
|
||||
|
||||
curl_easy_setopt(
|
||||
curl,
|
||||
CURLOPT_POST,
|
||||
1L
|
||||
);
|
||||
|
||||
curl_easy_setopt(
|
||||
curl,
|
||||
CURLOPT_POSTFIELDS,
|
||||
body.c_str()
|
||||
);
|
||||
|
||||
curl_easy_setopt(
|
||||
curl,
|
||||
CURLOPT_WRITEFUNCTION,
|
||||
writeCallback
|
||||
);
|
||||
|
||||
curl_easy_setopt(
|
||||
curl,
|
||||
CURLOPT_WRITEDATA,
|
||||
&response
|
||||
);
|
||||
|
||||
curl_easy_setopt(
|
||||
curl,
|
||||
CURLOPT_CONNECTTIMEOUT,
|
||||
5L
|
||||
);
|
||||
|
||||
curl_easy_setopt(
|
||||
curl,
|
||||
CURLOPT_TIMEOUT,
|
||||
120L
|
||||
);
|
||||
|
||||
const CURLcode ret =
|
||||
curl_easy_perform(curl);
|
||||
|
||||
long status = 0;
|
||||
|
||||
curl_easy_getinfo(
|
||||
curl,
|
||||
CURLINFO_RESPONSE_CODE,
|
||||
&status
|
||||
);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (ret != CURLE_OK) {
|
||||
throw std::runtime_error(
|
||||
std::string("HTTP request failed: ") +
|
||||
curl_easy_strerror(ret)
|
||||
);
|
||||
}
|
||||
|
||||
if (status < 200 || status >= 300) {
|
||||
throw std::runtime_error(
|
||||
"LLM HTTP status " +
|
||||
std::to_string(status) +
|
||||
"\nResponse:\n" +
|
||||
response
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
std::string LLM::parseContent(
|
||||
const std::string& json) const
|
||||
{
|
||||
const std::string key = "\"content\"";
|
||||
|
||||
const size_t key_pos =
|
||||
json.find(key);
|
||||
|
||||
if (key_pos == std::string::npos)
|
||||
return {};
|
||||
|
||||
const size_t colon =
|
||||
json.find(
|
||||
':',
|
||||
key_pos + key.size()
|
||||
);
|
||||
|
||||
if (colon == std::string::npos)
|
||||
return {};
|
||||
|
||||
const size_t quote =
|
||||
json.find(
|
||||
'"',
|
||||
colon + 1
|
||||
);
|
||||
|
||||
if (quote == std::string::npos)
|
||||
return {};
|
||||
|
||||
std::string result;
|
||||
result.reserve(256);
|
||||
|
||||
bool escaped = false;
|
||||
|
||||
for (size_t i = quote + 1;
|
||||
i < json.size();
|
||||
++i) {
|
||||
|
||||
const char c = json[i];
|
||||
|
||||
if (escaped) {
|
||||
switch (c) {
|
||||
case 'n':
|
||||
result += '\n';
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
result += '\r';
|
||||
break;
|
||||
|
||||
case 't':
|
||||
result += '\t';
|
||||
break;
|
||||
|
||||
case '"':
|
||||
result += '"';
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
result += '\\';
|
||||
break;
|
||||
|
||||
case '/':
|
||||
result += '/';
|
||||
break;
|
||||
|
||||
default:
|
||||
result += c;
|
||||
break;
|
||||
}
|
||||
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"')
|
||||
break;
|
||||
|
||||
result += c;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string removeThinkBlocks(
|
||||
std::string text)
|
||||
{
|
||||
for (;;) {
|
||||
const size_t begin =
|
||||
text.find("<think>");
|
||||
|
||||
if (begin == std::string::npos)
|
||||
break;
|
||||
|
||||
const size_t end =
|
||||
text.find(
|
||||
"</think>",
|
||||
begin
|
||||
);
|
||||
|
||||
if (end == std::string::npos) {
|
||||
text.erase(begin);
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t len =
|
||||
end +
|
||||
std::string("</think>").size() -
|
||||
begin;
|
||||
|
||||
text.erase(begin, len);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string LLM::chat(
|
||||
const std::string& user)
|
||||
{
|
||||
history_.push_back({
|
||||
"user",
|
||||
user
|
||||
});
|
||||
|
||||
std::ostringstream body;
|
||||
|
||||
body
|
||||
<< '{'
|
||||
<< "\"model\":\""
|
||||
<< escapeJson(config_.model)
|
||||
<< "\","
|
||||
|
||||
<< "\"messages\":["
|
||||
|
||||
<< '{'
|
||||
<< "\"role\":\"system\","
|
||||
<< "\"content\":\""
|
||||
<< escapeJson(config_.system_prompt)
|
||||
<< "\""
|
||||
<< '}';
|
||||
|
||||
const size_t begin =
|
||||
history_.size() > 12
|
||||
? history_.size() - 12
|
||||
: 0;
|
||||
|
||||
for (size_t i = begin;
|
||||
i < history_.size();
|
||||
++i) {
|
||||
|
||||
body
|
||||
<< ",{"
|
||||
<< "\"role\":\""
|
||||
<< escapeJson(
|
||||
history_[i].role
|
||||
)
|
||||
<< "\","
|
||||
<< "\"content\":\""
|
||||
<< escapeJson(
|
||||
history_[i].content
|
||||
)
|
||||
<< "\""
|
||||
<< '}';
|
||||
}
|
||||
|
||||
body
|
||||
<< "],"
|
||||
|
||||
<< "\"max_tokens\":"
|
||||
<< config_.max_tokens
|
||||
<< ','
|
||||
|
||||
<< "\"temperature\":"
|
||||
<< config_.temperature
|
||||
<< ','
|
||||
|
||||
<< "\"chat_template_kwargs\":{"
|
||||
<< "\"enable_thinking\":false"
|
||||
<< '}'
|
||||
|
||||
<< '}';
|
||||
|
||||
const std::string response =
|
||||
httpPost(body.str());
|
||||
|
||||
std::string answer =
|
||||
parseContent(response);
|
||||
|
||||
answer =
|
||||
removeThinkBlocks(answer);
|
||||
|
||||
if (answer.empty()) {
|
||||
std::cerr
|
||||
<< "LLM returned empty content:\n"
|
||||
<< response
|
||||
<< '\n';
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
history_.push_back({
|
||||
"assistant",
|
||||
answer
|
||||
});
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
} // namespace r1_voice
|
||||
@@ -0,0 +1,205 @@
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
#include <unitree/robot/go2/video/video_client.hpp>
|
||||
|
||||
#include "r1_vision/robot_common.hpp"
|
||||
#include "r1_vision/kinematics.hpp"
|
||||
|
||||
using namespace r1_vision;
|
||||
|
||||
struct ClickState {
|
||||
bool clicked = false;
|
||||
cv::Point point{};
|
||||
};
|
||||
|
||||
static void MouseCallback(int event, int x, int y, int, void* user) {
|
||||
auto* s = static_cast<ClickState*>(user);
|
||||
if (event == cv::EVENT_LBUTTONDOWN) {
|
||||
s->clicked = true;
|
||||
s->point = cv::Point(x, y);
|
||||
std::cout << "[CLICK] " << x << ", " << y << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
static cv::Mat GetFrame(unitree::robot::go2::VideoClient& camera) {
|
||||
std::vector<uint8_t> jpeg;
|
||||
if (camera.GetImageSample(jpeg) != 0 || jpeg.empty()) return {};
|
||||
cv::Mat encoded(1, static_cast<int>(jpeg.size()), CV_8UC1, jpeg.data());
|
||||
return cv::imdecode(encoded, cv::IMREAD_COLOR);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: r1_hand_calibrate <network_interface> <camera_intrinsics.yaml>\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
cv::FileStorage fs(argv[2], cv::FILE_STORAGE_READ);
|
||||
if (!fs.isOpened()) {
|
||||
std::cerr << "Cannot open intrinsics: " << argv[2] << std::endl;
|
||||
return 1;
|
||||
}
|
||||
cv::Mat K, D;
|
||||
fs["camera_matrix"] >> K;
|
||||
fs["distortion"] >> D;
|
||||
fs.release();
|
||||
|
||||
if (K.empty() || D.empty()) {
|
||||
std::cerr << "Invalid camera calibration file." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
pinocchio::Model model;
|
||||
pinocchio::urdf::buildModel(
|
||||
"/home/jvizner/unitree_ros/robots/r1_description/R1.urdf",
|
||||
model);
|
||||
pinocchio::Data data(model);
|
||||
|
||||
RobotController robot(argv[1]);
|
||||
|
||||
unitree::robot::go2::VideoClient camera;
|
||||
camera.SetTimeout(1.0f);
|
||||
camera.Init();
|
||||
|
||||
const std::string window = "R1 Wrist Extrinsic Calibration";
|
||||
cv::namedWindow(window, cv::WINDOW_NORMAL);
|
||||
ClickState click;
|
||||
cv::setMouseCallback(window, MouseCallback, &click);
|
||||
|
||||
std::vector<cv::Point3f> robot_points;
|
||||
std::vector<cv::Point2f> image_points;
|
||||
|
||||
std::cout << "\nControls:\n"
|
||||
<< " left click = mark LEFT wrist\n"
|
||||
<< " c = capture point\n"
|
||||
<< " p/P = left shoulder pitch +/- 0.08 rad\n"
|
||||
<< " r/R = left shoulder roll +/- 0.08 rad\n"
|
||||
<< " y/Y = left shoulder yaw +/- 0.08 rad\n"
|
||||
<< " e/E = left elbow +/- 0.08 rad\n"
|
||||
<< " q = solve and quit\n";
|
||||
|
||||
while (true) {
|
||||
cv::Mat frame = GetFrame(camera);
|
||||
if (frame.empty()) {
|
||||
usleep(10000);
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto q = robot.CurrentQ();
|
||||
const Eigen::Vector3d wrist = FramePosition(
|
||||
model, data, q, "left_wrist_roll_link");
|
||||
|
||||
cv::Mat display = frame.clone();
|
||||
cv::putText(display,
|
||||
"samples=" + std::to_string(image_points.size()),
|
||||
{20, 35}, cv::FONT_HERSHEY_SIMPLEX, 0.8,
|
||||
{0, 255, 0}, 2);
|
||||
|
||||
cv::putText(display,
|
||||
"wrist R1: " + std::to_string(wrist.x()) + ", " +
|
||||
std::to_string(wrist.y()) + ", " +
|
||||
std::to_string(wrist.z()),
|
||||
{20, 70}, cv::FONT_HERSHEY_SIMPLEX, 0.55,
|
||||
{255, 255, 0}, 2);
|
||||
|
||||
if (click.clicked)
|
||||
cv::circle(display, click.point, 7, {0, 0, 255}, -1);
|
||||
|
||||
cv::imshow(window, display);
|
||||
const int key = cv::waitKey(10) & 0xff;
|
||||
|
||||
if (key == 'q') break;
|
||||
|
||||
if (key == 'c') {
|
||||
if (!click.clicked) {
|
||||
std::cout << "[WARN] Click the wrist first." << std::endl;
|
||||
continue;
|
||||
}
|
||||
image_points.emplace_back(
|
||||
static_cast<float>(click.point.x),
|
||||
static_cast<float>(click.point.y));
|
||||
robot_points.emplace_back(
|
||||
static_cast<float>(wrist.x()),
|
||||
static_cast<float>(wrist.y()),
|
||||
static_cast<float>(wrist.z()));
|
||||
std::cout << "[CAPTURE] #" << image_points.size()
|
||||
<< " pixel=" << click.point.x << "," << click.point.y
|
||||
<< " robot=" << wrist.transpose() << std::endl;
|
||||
click.clicked = false;
|
||||
}
|
||||
|
||||
constexpr float step = 0.08f;
|
||||
switch (key) {
|
||||
case 'p': robot.MoveJointRelative(0, +step); break;
|
||||
case 'P': robot.MoveJointRelative(0, -step); break;
|
||||
case 'r': robot.MoveJointRelative(1, +step); break;
|
||||
case 'R': robot.MoveJointRelative(1, -step); break;
|
||||
case 'y': robot.MoveJointRelative(2, +step); break;
|
||||
case 'Y': robot.MoveJointRelative(2, -step); break;
|
||||
case 'e': robot.MoveJointRelative(3, +step); break;
|
||||
case 'E': robot.MoveJointRelative(3, -step); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
cv::destroyAllWindows();
|
||||
|
||||
if (image_points.size() < 8) {
|
||||
std::cerr << "Need at least 8 captured points; got "
|
||||
<< image_points.size() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
cv::Mat rvec, tvec;
|
||||
std::vector<int> inliers;
|
||||
if (!cv::solvePnPRansac(
|
||||
robot_points, image_points, K, D,
|
||||
rvec, tvec, false, 200, 5.0, 0.99,
|
||||
inliers, cv::SOLVEPNP_ITERATIVE)) {
|
||||
std::cerr << "solvePnPRansac failed." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
cv::Mat R;
|
||||
cv::Rodrigues(rvec, R);
|
||||
|
||||
std::vector<cv::Point2f> projected;
|
||||
cv::projectPoints(robot_points, rvec, tvec, K, D, projected);
|
||||
|
||||
double sum_sq = 0.0;
|
||||
for (size_t i = 0; i < image_points.size(); ++i) {
|
||||
const double dx = image_points[i].x - projected[i].x;
|
||||
const double dy = image_points[i].y - projected[i].y;
|
||||
sum_sq += dx * dx + dy * dy;
|
||||
}
|
||||
const double rms = std::sqrt(sum_sq / image_points.size());
|
||||
|
||||
std::cout << "\n========================================\n"
|
||||
<< "EXTRINSIC CALIBRATION\n"
|
||||
<< "========================================\n"
|
||||
<< "R robot->camera:\n" << R << "\n"
|
||||
<< "t robot->camera:\n" << tvec << "\n"
|
||||
<< "RMS reprojection error = " << rms << " px\n"
|
||||
<< "Inliers = " << inliers.size() << " / " << image_points.size()
|
||||
<< std::endl;
|
||||
|
||||
const std::string output =
|
||||
"/home/jvizner/r1_camera_calibration/camera_extrinsics.yaml";
|
||||
|
||||
cv::FileStorage out(output, cv::FILE_STORAGE_WRITE);
|
||||
out << "camera_matrix" << K;
|
||||
out << "distortion" << D;
|
||||
out << "rotation_robot_to_camera" << R;
|
||||
out << "translation_robot_to_camera" << tvec;
|
||||
out << "rms_reprojection_error" << rms;
|
||||
out.release();
|
||||
|
||||
std::cout << "[SYS] Saved: " << output << std::endl;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <ifaddrs.h>
|
||||
#include <iostream>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
constexpr const char* GROUP_IP = "239.168.123.161";
|
||||
constexpr int PORT = 5555;
|
||||
|
||||
constexpr int SAMPLE_RATE = 16000;
|
||||
constexpr int SECONDS = 5;
|
||||
constexpr int PACKET_BYTES = 5120;
|
||||
constexpr int TOTAL_BYTES =
|
||||
SAMPLE_RATE * 2 * SECONDS;
|
||||
|
||||
static std::string get_local_ip(
|
||||
const std::string& interface)
|
||||
{
|
||||
ifaddrs* ifaddr = nullptr;
|
||||
|
||||
if (getifaddrs(&ifaddr) != 0)
|
||||
return {};
|
||||
|
||||
std::string result;
|
||||
|
||||
for (ifaddrs* ifa = ifaddr;
|
||||
ifa;
|
||||
ifa = ifa->ifa_next) {
|
||||
|
||||
if (!ifa->ifa_addr)
|
||||
continue;
|
||||
|
||||
if (interface != ifa->ifa_name)
|
||||
continue;
|
||||
|
||||
if (ifa->ifa_addr->sa_family != AF_INET)
|
||||
continue;
|
||||
|
||||
char host[NI_MAXHOST]{};
|
||||
|
||||
if (getnameinfo(
|
||||
ifa->ifa_addr,
|
||||
sizeof(sockaddr_in),
|
||||
host,
|
||||
sizeof(host),
|
||||
nullptr,
|
||||
0,
|
||||
NI_NUMERICHOST) == 0) {
|
||||
|
||||
result = host;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
freeifaddrs(ifaddr);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void write_wav(
|
||||
const std::string& path,
|
||||
const std::vector<int16_t>& pcm)
|
||||
{
|
||||
std::ofstream f(
|
||||
path,
|
||||
std::ios::binary
|
||||
);
|
||||
|
||||
const uint32_t data_size =
|
||||
pcm.size() * sizeof(int16_t);
|
||||
|
||||
const uint32_t riff_size =
|
||||
36 + data_size;
|
||||
|
||||
const uint16_t format = 1;
|
||||
const uint16_t channels = 1;
|
||||
const uint16_t bits = 16;
|
||||
|
||||
const uint32_t sample_rate = 16000;
|
||||
|
||||
const uint16_t block_align =
|
||||
channels * bits / 8;
|
||||
|
||||
const uint32_t byte_rate =
|
||||
sample_rate * block_align;
|
||||
|
||||
f.write("RIFF", 4);
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(&riff_size),
|
||||
4
|
||||
);
|
||||
|
||||
f.write("WAVE", 4);
|
||||
f.write("fmt ", 4);
|
||||
|
||||
const uint32_t fmt_size = 16;
|
||||
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(&fmt_size),
|
||||
4
|
||||
);
|
||||
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(&format),
|
||||
2
|
||||
);
|
||||
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(&channels),
|
||||
2
|
||||
);
|
||||
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(&sample_rate),
|
||||
4
|
||||
);
|
||||
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(&byte_rate),
|
||||
4
|
||||
);
|
||||
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(&block_align),
|
||||
2
|
||||
);
|
||||
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(&bits),
|
||||
2
|
||||
);
|
||||
|
||||
f.write("data", 4);
|
||||
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(&data_size),
|
||||
4
|
||||
);
|
||||
|
||||
f.write(
|
||||
reinterpret_cast<const char*>(pcm.data()),
|
||||
data_size
|
||||
);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc != 2) {
|
||||
std::cerr
|
||||
<< "Usage: "
|
||||
<< argv[0]
|
||||
<< " <interface>\n";
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string interface = argv[1];
|
||||
|
||||
const std::string local_ip =
|
||||
get_local_ip(interface);
|
||||
|
||||
std::cout
|
||||
<< "Interface: "
|
||||
<< interface
|
||||
<< '\n'
|
||||
<< "Local IP: "
|
||||
<< local_ip
|
||||
<< '\n';
|
||||
|
||||
int sock =
|
||||
socket(AF_INET, SOCK_DGRAM, 0);
|
||||
|
||||
if (sock < 0) {
|
||||
perror("socket");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int reuse = 1;
|
||||
|
||||
setsockopt(
|
||||
sock,
|
||||
SOL_SOCKET,
|
||||
SO_REUSEADDR,
|
||||
&reuse,
|
||||
sizeof(reuse)
|
||||
);
|
||||
|
||||
sockaddr_in addr{};
|
||||
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT);
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
|
||||
if (bind(
|
||||
sock,
|
||||
reinterpret_cast<sockaddr*>(&addr),
|
||||
sizeof(addr)) != 0) {
|
||||
|
||||
perror("bind");
|
||||
close(sock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
ip_mreq mreq{};
|
||||
|
||||
inet_pton(
|
||||
AF_INET,
|
||||
GROUP_IP,
|
||||
&mreq.imr_multiaddr
|
||||
);
|
||||
|
||||
inet_pton(
|
||||
AF_INET,
|
||||
local_ip.c_str(),
|
||||
&mreq.imr_interface
|
||||
);
|
||||
|
||||
if (setsockopt(
|
||||
sock,
|
||||
IPPROTO_IP,
|
||||
IP_ADD_MEMBERSHIP,
|
||||
&mreq,
|
||||
sizeof(mreq)) != 0) {
|
||||
|
||||
perror("IP_ADD_MEMBERSHIP");
|
||||
close(sock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<int16_t> pcm;
|
||||
pcm.reserve(SAMPLE_RATE * SECONDS);
|
||||
|
||||
int total_bytes = 0;
|
||||
|
||||
std::cout
|
||||
<< "\nSpeak for 5 seconds...\n";
|
||||
|
||||
while (total_bytes < TOTAL_BYTES) {
|
||||
|
||||
char buffer[PACKET_BYTES];
|
||||
|
||||
const ssize_t len =
|
||||
recvfrom(
|
||||
sock,
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
0,
|
||||
nullptr,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (len <= 0)
|
||||
continue;
|
||||
|
||||
const size_t samples =
|
||||
len / sizeof(int16_t);
|
||||
|
||||
const int16_t* p =
|
||||
reinterpret_cast<
|
||||
const int16_t*
|
||||
>(buffer);
|
||||
|
||||
pcm.insert(
|
||||
pcm.end(),
|
||||
p,
|
||||
p + samples
|
||||
);
|
||||
|
||||
total_bytes += len;
|
||||
|
||||
std::cout
|
||||
<< "\r"
|
||||
<< total_bytes
|
||||
<< " / "
|
||||
<< TOTAL_BYTES
|
||||
<< " bytes"
|
||||
<< std::flush;
|
||||
}
|
||||
|
||||
std::cout << '\n';
|
||||
|
||||
write_wav(
|
||||
"/tmp/r1_raw_test.wav",
|
||||
pcm
|
||||
);
|
||||
|
||||
std::cout
|
||||
<< "Written /tmp/r1_raw_test.wav\n";
|
||||
|
||||
setsockopt(
|
||||
sock,
|
||||
IPPROTO_IP,
|
||||
IP_DROP_MEMBERSHIP,
|
||||
&mreq,
|
||||
sizeof(mreq)
|
||||
);
|
||||
|
||||
close(sock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
+658
@@ -0,0 +1,658 @@
|
||||
#include "r1_voice/stt.hh"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <ifaddrs.h>
|
||||
#include <iostream>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
namespace r1_voice {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* GROUP_IP =
|
||||
"239.168.123.161";
|
||||
|
||||
constexpr uint16_t PORT = 5555;
|
||||
|
||||
constexpr int SAMPLE_RATE = 16000;
|
||||
|
||||
std::string findR1InterfaceAddress(
|
||||
const std::string& interface_name)
|
||||
{
|
||||
struct ifaddrs* ifaddr = nullptr;
|
||||
|
||||
if (getifaddrs(&ifaddr) != 0)
|
||||
return {};
|
||||
|
||||
std::string result;
|
||||
|
||||
for (auto* ifa = ifaddr;
|
||||
ifa != nullptr;
|
||||
ifa = ifa->ifa_next) {
|
||||
|
||||
if (!ifa->ifa_addr)
|
||||
continue;
|
||||
|
||||
if (interface_name != ifa->ifa_name)
|
||||
continue;
|
||||
|
||||
if (ifa->ifa_addr->sa_family != AF_INET)
|
||||
continue;
|
||||
|
||||
char host[NI_MAXHOST] = {};
|
||||
|
||||
if (getnameinfo(
|
||||
ifa->ifa_addr,
|
||||
sizeof(sockaddr_in),
|
||||
host,
|
||||
sizeof(host),
|
||||
nullptr,
|
||||
0,
|
||||
NI_NUMERICHOST) == 0) {
|
||||
|
||||
result = host;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
freeifaddrs(ifaddr);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
float rms(
|
||||
const int16_t* samples,
|
||||
size_t count)
|
||||
{
|
||||
if (count == 0)
|
||||
return 0.0f;
|
||||
|
||||
double sum = 0.0;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const float sample =
|
||||
static_cast<float>(samples[i]) /
|
||||
32768.0f;
|
||||
|
||||
sum +=
|
||||
static_cast<double>(sample) *
|
||||
static_cast<double>(sample);
|
||||
}
|
||||
|
||||
return static_cast<float>(
|
||||
std::sqrt(sum / count)
|
||||
);
|
||||
}
|
||||
|
||||
void writeWav(
|
||||
const std::string& filename,
|
||||
const std::vector<int16_t>& pcm)
|
||||
{
|
||||
std::ofstream file(
|
||||
filename,
|
||||
std::ios::binary
|
||||
);
|
||||
|
||||
if (!file)
|
||||
throw std::runtime_error(
|
||||
"Cannot open WAV file: " + filename
|
||||
);
|
||||
|
||||
const uint16_t audio_format = 1;
|
||||
const uint16_t channels = 1;
|
||||
const uint16_t bits_per_sample = 16;
|
||||
|
||||
const uint32_t sample_rate = SAMPLE_RATE;
|
||||
|
||||
const uint16_t block_align =
|
||||
channels * bits_per_sample / 8;
|
||||
|
||||
const uint32_t byte_rate =
|
||||
sample_rate * block_align;
|
||||
|
||||
const uint32_t data_size =
|
||||
static_cast<uint32_t>(
|
||||
pcm.size() * sizeof(int16_t)
|
||||
);
|
||||
|
||||
const uint32_t riff_size =
|
||||
36 + data_size;
|
||||
|
||||
file.write(
|
||||
"RIFF",
|
||||
4
|
||||
);
|
||||
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(&riff_size),
|
||||
sizeof(riff_size)
|
||||
);
|
||||
|
||||
file.write(
|
||||
"WAVE",
|
||||
4
|
||||
);
|
||||
|
||||
file.write(
|
||||
"fmt ",
|
||||
4
|
||||
);
|
||||
|
||||
const uint32_t fmt_size = 16;
|
||||
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(&fmt_size),
|
||||
sizeof(fmt_size)
|
||||
);
|
||||
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(&audio_format),
|
||||
sizeof(audio_format)
|
||||
);
|
||||
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(&channels),
|
||||
sizeof(channels)
|
||||
);
|
||||
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(&sample_rate),
|
||||
sizeof(sample_rate)
|
||||
);
|
||||
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(&byte_rate),
|
||||
sizeof(byte_rate)
|
||||
);
|
||||
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(&block_align),
|
||||
sizeof(block_align)
|
||||
);
|
||||
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(&bits_per_sample),
|
||||
sizeof(bits_per_sample)
|
||||
);
|
||||
|
||||
file.write(
|
||||
"data",
|
||||
4
|
||||
);
|
||||
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(&data_size),
|
||||
sizeof(data_size)
|
||||
);
|
||||
|
||||
if (!pcm.empty()) {
|
||||
file.write(
|
||||
reinterpret_cast<const char*>(pcm.data()),
|
||||
static_cast<std::streamsize>(
|
||||
data_size
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpeechToText::SpeechToText(
|
||||
SttConfig config)
|
||||
: config_(std::move(config))
|
||||
{
|
||||
}
|
||||
|
||||
std::string SpeechToText::listen()
|
||||
{
|
||||
constexpr const char* WAV_PATH =
|
||||
"/tmp/r1_voice_record.wav";
|
||||
|
||||
if (!recordToWav(WAV_PATH))
|
||||
return {};
|
||||
|
||||
return transcribe(WAV_PATH);
|
||||
}
|
||||
|
||||
bool SpeechToText::recordToWav(
|
||||
const std::string& path)
|
||||
{
|
||||
const int sock =
|
||||
socket(
|
||||
AF_INET,
|
||||
SOCK_DGRAM,
|
||||
0
|
||||
);
|
||||
|
||||
if (sock < 0) {
|
||||
std::cerr
|
||||
<< "socket() failed: "
|
||||
<< std::strerror(errno)
|
||||
<< '\n';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int reuse = 1;
|
||||
|
||||
if (setsockopt(
|
||||
sock,
|
||||
SOL_SOCKET,
|
||||
SO_REUSEADDR,
|
||||
&reuse,
|
||||
sizeof(reuse)) != 0) {
|
||||
|
||||
std::cerr
|
||||
<< "SO_REUSEADDR failed: "
|
||||
<< std::strerror(errno)
|
||||
<< '\n';
|
||||
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
sockaddr_in local_addr{};
|
||||
|
||||
local_addr.sin_family =
|
||||
AF_INET;
|
||||
|
||||
local_addr.sin_port =
|
||||
htons(PORT);
|
||||
|
||||
local_addr.sin_addr.s_addr =
|
||||
htonl(INADDR_ANY);
|
||||
|
||||
if (bind(
|
||||
sock,
|
||||
reinterpret_cast<sockaddr*>(&local_addr),
|
||||
sizeof(local_addr)) != 0) {
|
||||
|
||||
std::cerr
|
||||
<< "bind() failed: "
|
||||
<< std::strerror(errno)
|
||||
<< '\n';
|
||||
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* env_interface =
|
||||
std::getenv(
|
||||
"R1_NETWORK_INTERFACE"
|
||||
);
|
||||
|
||||
if (!env_interface) {
|
||||
std::cerr
|
||||
<< "R1_NETWORK_INTERFACE is not set\n"
|
||||
<< "Example:\n"
|
||||
<< " export R1_NETWORK_INTERFACE=enp3s0\n";
|
||||
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string interface =
|
||||
env_interface;
|
||||
|
||||
const std::string local_ip =
|
||||
findR1InterfaceAddress(
|
||||
interface
|
||||
);
|
||||
|
||||
if (local_ip.empty()) {
|
||||
std::cerr
|
||||
<< "Could not find IPv4 address for "
|
||||
<< interface
|
||||
<< '\n';
|
||||
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
ip_mreq mreq{};
|
||||
|
||||
if (inet_pton(
|
||||
AF_INET,
|
||||
GROUP_IP,
|
||||
&mreq.imr_multiaddr) != 1) {
|
||||
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inet_pton(
|
||||
AF_INET,
|
||||
local_ip.c_str(),
|
||||
&mreq.imr_interface) != 1) {
|
||||
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (setsockopt(
|
||||
sock,
|
||||
IPPROTO_IP,
|
||||
IP_ADD_MEMBERSHIP,
|
||||
&mreq,
|
||||
sizeof(mreq)) != 0) {
|
||||
|
||||
std::cerr
|
||||
<< "IP_ADD_MEMBERSHIP failed: "
|
||||
<< std::strerror(errno)
|
||||
<< '\n';
|
||||
|
||||
close(sock);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* R1 sends 5120 byte packets.
|
||||
*/
|
||||
constexpr size_t PACKET_SIZE = 5120;
|
||||
|
||||
std::vector<int16_t> audio;
|
||||
|
||||
const size_t reserve_samples =
|
||||
static_cast<size_t>(
|
||||
SAMPLE_RATE *
|
||||
config_.max_record_ms /
|
||||
1000
|
||||
);
|
||||
|
||||
audio.reserve(reserve_samples);
|
||||
|
||||
std::vector<int16_t> preroll;
|
||||
|
||||
const size_t preroll_samples =
|
||||
static_cast<size_t>(
|
||||
SAMPLE_RATE *
|
||||
config_.preroll_ms /
|
||||
1000
|
||||
);
|
||||
|
||||
preroll.reserve(preroll_samples);
|
||||
|
||||
char buffer[PACKET_SIZE];
|
||||
|
||||
bool speaking = false;
|
||||
|
||||
int speech_ms = 0;
|
||||
int silence_ms = 0;
|
||||
int total_ms = 0;
|
||||
|
||||
std::cout
|
||||
<< "\nListening...\n";
|
||||
|
||||
while (total_ms <
|
||||
config_.max_record_ms) {
|
||||
|
||||
const ssize_t len =
|
||||
recvfrom(
|
||||
sock,
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
0,
|
||||
nullptr,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (len <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((len % 2) != 0)
|
||||
continue;
|
||||
|
||||
const auto* samples =
|
||||
reinterpret_cast<
|
||||
const int16_t*
|
||||
>(buffer);
|
||||
|
||||
const size_t sample_count =
|
||||
static_cast<size_t>(len) / 2;
|
||||
|
||||
const int packet_ms =
|
||||
static_cast<int>(
|
||||
sample_count * 1000 /
|
||||
SAMPLE_RATE
|
||||
);
|
||||
|
||||
total_ms += packet_ms;
|
||||
|
||||
const float level =
|
||||
rms(
|
||||
samples,
|
||||
sample_count
|
||||
);
|
||||
|
||||
if (!speaking) {
|
||||
|
||||
preroll.insert(
|
||||
preroll.end(),
|
||||
samples,
|
||||
samples + sample_count
|
||||
);
|
||||
|
||||
if (preroll.size() >
|
||||
preroll_samples) {
|
||||
|
||||
const size_t remove =
|
||||
preroll.size() -
|
||||
preroll_samples;
|
||||
|
||||
preroll.erase(
|
||||
preroll.begin(),
|
||||
preroll.begin() +
|
||||
static_cast<
|
||||
std::ptrdiff_t
|
||||
>(remove)
|
||||
);
|
||||
}
|
||||
|
||||
if (level >=
|
||||
config_.speech_threshold) {
|
||||
|
||||
speaking = true;
|
||||
|
||||
audio.insert(
|
||||
audio.end(),
|
||||
preroll.begin(),
|
||||
preroll.end()
|
||||
);
|
||||
|
||||
preroll.clear();
|
||||
|
||||
speech_ms += packet_ms;
|
||||
|
||||
std::cout
|
||||
<< "Speech detected\n";
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
audio.insert(
|
||||
audio.end(),
|
||||
samples,
|
||||
samples + sample_count
|
||||
);
|
||||
|
||||
speech_ms += packet_ms;
|
||||
|
||||
if (level <
|
||||
config_.speech_threshold) {
|
||||
|
||||
silence_ms += packet_ms;
|
||||
}
|
||||
else {
|
||||
silence_ms = 0;
|
||||
}
|
||||
|
||||
if (
|
||||
speech_ms >=
|
||||
config_.min_speech_ms &&
|
||||
silence_ms >=
|
||||
config_.silence_ms
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setsockopt(
|
||||
sock,
|
||||
IPPROTO_IP,
|
||||
IP_DROP_MEMBERSHIP,
|
||||
&mreq,
|
||||
sizeof(mreq)
|
||||
);
|
||||
|
||||
close(sock);
|
||||
|
||||
if (!speaking ||
|
||||
audio.empty()) {
|
||||
|
||||
std::cout
|
||||
<< "No speech detected.\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
writeWav(
|
||||
path,
|
||||
audio
|
||||
);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr
|
||||
<< "WAV write failed: "
|
||||
<< e.what()
|
||||
<< '\n';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout
|
||||
<< "Recorded "
|
||||
<< audio.size() /
|
||||
static_cast<double>(
|
||||
SAMPLE_RATE
|
||||
)
|
||||
<< " seconds\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string SpeechToText::transcribe(
|
||||
const std::string& wav_path)
|
||||
{
|
||||
std::ostringstream command;
|
||||
|
||||
command
|
||||
<< '"'
|
||||
<< config_.whisper_cli
|
||||
<< '"'
|
||||
<< " -m "
|
||||
<< '"'
|
||||
<< config_.whisper_model
|
||||
<< '"'
|
||||
<< " -f "
|
||||
<< '"'
|
||||
<< wav_path
|
||||
<< '"'
|
||||
<< " -l "
|
||||
<< config_.language
|
||||
<< " -t "
|
||||
<< config_.threads
|
||||
<< " --no-timestamps"
|
||||
<< " -nt"
|
||||
<< " 2>/dev/null";
|
||||
|
||||
std::cout
|
||||
<< "Transcribing...\n";
|
||||
|
||||
FILE* pipe =
|
||||
popen(
|
||||
command.str().c_str(),
|
||||
"r"
|
||||
);
|
||||
|
||||
if (!pipe) {
|
||||
std::cerr
|
||||
<< "Failed to start whisper-cli\n";
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
char buffer[4096];
|
||||
|
||||
std::string output;
|
||||
|
||||
while (fgets(
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
pipe)) {
|
||||
|
||||
output += buffer;
|
||||
}
|
||||
|
||||
const int status =
|
||||
pclose(pipe);
|
||||
|
||||
if (status != 0) {
|
||||
std::cerr
|
||||
<< "whisper-cli failed: "
|
||||
<< status
|
||||
<< '\n';
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::string text =
|
||||
trim(output);
|
||||
|
||||
if (!text.empty()) {
|
||||
std::cout
|
||||
<< "You: "
|
||||
<< text
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string SpeechToText::trim(
|
||||
const std::string& text)
|
||||
{
|
||||
const auto first =
|
||||
text.find_first_not_of(
|
||||
" \t\r\n"
|
||||
);
|
||||
|
||||
if (first == std::string::npos)
|
||||
return {};
|
||||
|
||||
const auto last =
|
||||
text.find_last_not_of(
|
||||
" \t\r\n"
|
||||
);
|
||||
|
||||
return text.substr(
|
||||
first,
|
||||
last - first + 1
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace r1_voice
|
||||
+672
@@ -0,0 +1,672 @@
|
||||
#include "r1_voice/tts.hh"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include <unitree/common/time/time_tool.hpp>
|
||||
#include <unitree/robot/channel/channel_factory.hpp>
|
||||
|
||||
namespace r1_voice {
|
||||
|
||||
namespace {
|
||||
|
||||
bool readU16(
|
||||
std::ifstream& f,
|
||||
uint16_t& value)
|
||||
{
|
||||
f.read(
|
||||
reinterpret_cast<char*>(&value),
|
||||
sizeof(value)
|
||||
);
|
||||
|
||||
return static_cast<bool>(f);
|
||||
}
|
||||
|
||||
bool readU32(
|
||||
std::ifstream& f,
|
||||
uint32_t& value)
|
||||
{
|
||||
f.read(
|
||||
reinterpret_cast<char*>(&value),
|
||||
sizeof(value)
|
||||
);
|
||||
|
||||
return static_cast<bool>(f);
|
||||
}
|
||||
|
||||
bool runCommand(
|
||||
const std::string& command)
|
||||
{
|
||||
const int status =
|
||||
std::system(command.c_str());
|
||||
|
||||
if (status == -1)
|
||||
return false;
|
||||
|
||||
return WIFEXITED(status) &&
|
||||
WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TextToSpeech::TextToSpeech(
|
||||
TtsConfig config)
|
||||
: config_(std::move(config))
|
||||
{
|
||||
}
|
||||
|
||||
bool TextToSpeech::init(
|
||||
const std::string& network_interface)
|
||||
{
|
||||
if (network_interface.empty()) {
|
||||
std::cerr
|
||||
<< "TTS: network interface is empty\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
/*
|
||||
* ChannelFactory MUST be initialized before
|
||||
* constructing AudioClient.
|
||||
*/
|
||||
unitree::robot::ChannelFactory::
|
||||
Instance()->Init(
|
||||
0,
|
||||
network_interface
|
||||
);
|
||||
|
||||
client_ =
|
||||
std::make_unique<
|
||||
unitree::robot::r1::AudioClient
|
||||
>();
|
||||
|
||||
client_->Init();
|
||||
|
||||
client_->SetTimeout(10.0f);
|
||||
|
||||
const int32_t ret =
|
||||
client_->SetVolume(
|
||||
static_cast<uint8_t>(
|
||||
config_.volume
|
||||
)
|
||||
);
|
||||
|
||||
if (ret != 0) {
|
||||
std::cerr
|
||||
<< "TTS: SetVolume failed: "
|
||||
<< ret
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
|
||||
std::cout
|
||||
<< "TTS initialized\n"
|
||||
<< " Piper model: "
|
||||
<< config_.model
|
||||
<< '\n'
|
||||
<< " R1 interface: "
|
||||
<< network_interface
|
||||
<< '\n';
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr
|
||||
<< "TTS init failed: "
|
||||
<< e.what()
|
||||
<< '\n';
|
||||
|
||||
client_.reset();
|
||||
initialized_ = false;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TextToSpeech::synthesize(
|
||||
const std::string& text,
|
||||
const std::string& wav_path)
|
||||
{
|
||||
const std::string text_path =
|
||||
"/tmp/r1_tts_input.txt";
|
||||
|
||||
{
|
||||
std::ofstream out(
|
||||
text_path,
|
||||
std::ios::binary
|
||||
);
|
||||
|
||||
if (!out) {
|
||||
std::cerr
|
||||
<< "Cannot create "
|
||||
<< text_path
|
||||
<< '\n';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
out << text;
|
||||
}
|
||||
|
||||
std::ostringstream command;
|
||||
|
||||
command
|
||||
<< "python -m piper"
|
||||
<< " --model "
|
||||
<< '\''
|
||||
<< config_.model
|
||||
<< '\''
|
||||
<< " --output_file "
|
||||
<< '\''
|
||||
<< wav_path
|
||||
<< '\''
|
||||
<< " < "
|
||||
<< '\''
|
||||
<< text_path
|
||||
<< '\''
|
||||
<< " >/dev/null 2>&1";
|
||||
|
||||
std::cout
|
||||
<< "Synthesizing Czech speech...\n";
|
||||
|
||||
if (!runCommand(command.str())) {
|
||||
std::cerr
|
||||
<< "Piper failed\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextToSpeech::readWavPcm(
|
||||
const std::string& wav_path,
|
||||
std::vector<uint8_t>& pcm,
|
||||
uint32_t& sample_rate,
|
||||
uint16_t& channels,
|
||||
uint16_t& bits_per_sample)
|
||||
{
|
||||
std::ifstream f(
|
||||
wav_path,
|
||||
std::ios::binary
|
||||
);
|
||||
|
||||
if (!f) {
|
||||
std::cerr
|
||||
<< "Cannot open WAV: "
|
||||
<< wav_path
|
||||
<< '\n';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
char riff[4];
|
||||
char wave[4];
|
||||
|
||||
f.read(riff, 4);
|
||||
|
||||
if (!f ||
|
||||
std::memcmp(riff, "RIFF", 4) != 0) {
|
||||
|
||||
std::cerr
|
||||
<< "Invalid RIFF file\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t riff_size = 0;
|
||||
|
||||
if (!readU32(f, riff_size))
|
||||
return false;
|
||||
|
||||
(void)riff_size;
|
||||
|
||||
f.read(wave, 4);
|
||||
|
||||
if (!f ||
|
||||
std::memcmp(wave, "WAVE", 4) != 0) {
|
||||
|
||||
std::cerr
|
||||
<< "Invalid WAVE file\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool found_fmt = false;
|
||||
bool found_data = false;
|
||||
|
||||
uint16_t audio_format = 0;
|
||||
|
||||
uint32_t data_size = 0;
|
||||
std::streampos data_position{};
|
||||
|
||||
while (f && (!found_fmt || !found_data)) {
|
||||
|
||||
char chunk_id[4];
|
||||
uint32_t chunk_size = 0;
|
||||
|
||||
f.read(
|
||||
chunk_id,
|
||||
sizeof(chunk_id)
|
||||
);
|
||||
|
||||
if (!f)
|
||||
break;
|
||||
|
||||
if (!readU32(f, chunk_size))
|
||||
return false;
|
||||
|
||||
if (std::memcmp(
|
||||
chunk_id,
|
||||
"fmt ",
|
||||
4) == 0) {
|
||||
|
||||
if (chunk_size < 16) {
|
||||
std::cerr
|
||||
<< "Invalid fmt chunk\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readU16(
|
||||
f,
|
||||
audio_format))
|
||||
return false;
|
||||
|
||||
if (!readU16(
|
||||
f,
|
||||
channels))
|
||||
return false;
|
||||
|
||||
if (!readU32(
|
||||
f,
|
||||
sample_rate))
|
||||
return false;
|
||||
|
||||
uint32_t byte_rate = 0;
|
||||
uint16_t block_align = 0;
|
||||
|
||||
if (!readU32(
|
||||
f,
|
||||
byte_rate))
|
||||
return false;
|
||||
|
||||
if (!readU16(
|
||||
f,
|
||||
block_align))
|
||||
return false;
|
||||
|
||||
if (!readU16(
|
||||
f,
|
||||
bits_per_sample))
|
||||
return false;
|
||||
|
||||
(void)byte_rate;
|
||||
(void)block_align;
|
||||
|
||||
if (chunk_size > 16) {
|
||||
f.seekg(
|
||||
chunk_size - 16,
|
||||
std::ios::cur
|
||||
);
|
||||
}
|
||||
|
||||
found_fmt = true;
|
||||
}
|
||||
else if (
|
||||
std::memcmp(
|
||||
chunk_id,
|
||||
"data",
|
||||
4) == 0) {
|
||||
|
||||
data_size = chunk_size;
|
||||
|
||||
data_position = f.tellg();
|
||||
|
||||
f.seekg(
|
||||
chunk_size,
|
||||
std::ios::cur
|
||||
);
|
||||
|
||||
found_data = true;
|
||||
}
|
||||
else {
|
||||
f.seekg(
|
||||
chunk_size,
|
||||
std::ios::cur
|
||||
);
|
||||
}
|
||||
|
||||
if (chunk_size & 1)
|
||||
f.seekg(
|
||||
1,
|
||||
std::ios::cur
|
||||
);
|
||||
}
|
||||
|
||||
if (!found_fmt ||
|
||||
!found_data) {
|
||||
|
||||
std::cerr
|
||||
<< "WAV missing fmt/data chunk\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (audio_format != 1) {
|
||||
std::cerr
|
||||
<< "WAV is not PCM\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (channels != 1) {
|
||||
std::cerr
|
||||
<< "WAV must be mono, got "
|
||||
<< channels
|
||||
<< '\n';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bits_per_sample != 16) {
|
||||
std::cerr
|
||||
<< "WAV must be 16-bit PCM, got "
|
||||
<< bits_per_sample
|
||||
<< '\n';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
f.seekg(data_position);
|
||||
|
||||
pcm.resize(data_size);
|
||||
|
||||
f.read(
|
||||
reinterpret_cast<char*>(pcm.data()),
|
||||
static_cast<std::streamsize>(
|
||||
data_size
|
||||
)
|
||||
);
|
||||
|
||||
if (!f) {
|
||||
std::cerr
|
||||
<< "Failed reading PCM data\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextToSpeech::playPcm(
|
||||
const std::vector<uint8_t>& pcm)
|
||||
{
|
||||
if (!client_ || pcm.empty())
|
||||
return false;
|
||||
|
||||
// 16 kHz, mono, signed 16-bit PCM:
|
||||
// 16000 samples/s * 2 bytes = 32000 bytes/s
|
||||
constexpr double BYTES_PER_SECOND = 32000.0;
|
||||
|
||||
// Same chunk size as Unitree's R1 audio example:
|
||||
// 96000 bytes = 3 seconds.
|
||||
constexpr size_t CHUNK_SIZE = 96000;
|
||||
|
||||
// Feed chunks faster than playback so R1 keeps
|
||||
// an audio buffer and does not underrun.
|
||||
constexpr int INTER_CHUNK_DELAY_MS = 1000;
|
||||
|
||||
// Extra time after predicted playback completion before
|
||||
// enabling the microphone again.
|
||||
constexpr int PLAYBACK_MARGIN_MS = 600;
|
||||
|
||||
const std::string stream_id =
|
||||
std::to_string(
|
||||
unitree::common::GetCurrentTimeMillisecond()
|
||||
);
|
||||
|
||||
const auto playback_start =
|
||||
std::chrono::steady_clock::now();
|
||||
|
||||
size_t offset = 0;
|
||||
size_t chunk_index = 0;
|
||||
|
||||
while (offset < pcm.size()) {
|
||||
|
||||
const size_t remaining =
|
||||
pcm.size() - offset;
|
||||
|
||||
const size_t chunk_size =
|
||||
std::min(
|
||||
CHUNK_SIZE,
|
||||
remaining
|
||||
);
|
||||
|
||||
std::vector<uint8_t> chunk(
|
||||
pcm.begin() +
|
||||
static_cast<std::ptrdiff_t>(offset),
|
||||
pcm.begin() +
|
||||
static_cast<std::ptrdiff_t>(
|
||||
offset + chunk_size
|
||||
)
|
||||
);
|
||||
|
||||
std::cout
|
||||
<< "Sending TTS chunk "
|
||||
<< chunk_index
|
||||
<< ": "
|
||||
<< chunk_size
|
||||
<< " bytes\n";
|
||||
|
||||
const int32_t ret =
|
||||
client_->PlayStream(
|
||||
config_.app_name,
|
||||
stream_id,
|
||||
chunk
|
||||
);
|
||||
|
||||
std::cout
|
||||
<< "PlayStream returned "
|
||||
<< ret
|
||||
<< '\n';
|
||||
|
||||
if (ret != 0) {
|
||||
std::cerr
|
||||
<< "PlayStream failed on chunk "
|
||||
<< chunk_index
|
||||
<< '\n';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += chunk_size;
|
||||
++chunk_index;
|
||||
|
||||
// Keep the R1 buffer filled ahead of playback.
|
||||
if (offset < pcm.size()) {
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(
|
||||
INTER_CHUNK_DELAY_MS
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::cout
|
||||
<< "All TTS chunks submitted\n";
|
||||
|
||||
/*
|
||||
* Work out when the complete utterance should finish
|
||||
* relative to when we submitted its first chunk.
|
||||
*/
|
||||
const auto total_audio_ms =
|
||||
std::chrono::milliseconds(
|
||||
static_cast<int64_t>(
|
||||
(static_cast<double>(pcm.size()) /
|
||||
BYTES_PER_SECOND) *
|
||||
1000.0
|
||||
)
|
||||
);
|
||||
|
||||
const auto now =
|
||||
std::chrono::steady_clock::now();
|
||||
|
||||
const auto elapsed =
|
||||
std::chrono::duration_cast<
|
||||
std::chrono::milliseconds
|
||||
>(now - playback_start);
|
||||
|
||||
auto remaining_playback =
|
||||
total_audio_ms - elapsed;
|
||||
|
||||
if (remaining_playback.count() < 0) {
|
||||
remaining_playback =
|
||||
std::chrono::milliseconds(0);
|
||||
}
|
||||
|
||||
const auto final_wait =
|
||||
remaining_playback +
|
||||
std::chrono::milliseconds(
|
||||
PLAYBACK_MARGIN_MS
|
||||
);
|
||||
|
||||
std::cout
|
||||
<< "Waiting "
|
||||
<< final_wait.count()
|
||||
<< " ms for R1 playback to finish\n";
|
||||
|
||||
std::this_thread::sleep_for(
|
||||
final_wait
|
||||
);
|
||||
|
||||
/*
|
||||
* IMPORTANT:
|
||||
* Don't PlayStop() here.
|
||||
*
|
||||
* We let the R1 finish the buffered stream naturally.
|
||||
*/
|
||||
std::cout
|
||||
<< "TTS playback finished\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextToSpeech::speak(
|
||||
const std::string& text)
|
||||
{
|
||||
if (!initialized_) {
|
||||
std::cerr
|
||||
<< "TTS is not initialized\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.empty())
|
||||
return true;
|
||||
|
||||
std::cout
|
||||
<< "R1: "
|
||||
<< text
|
||||
<< '\n';
|
||||
|
||||
const std::string piper_wav =
|
||||
config_.output_wav;
|
||||
|
||||
const std::string output_wav_16k =
|
||||
"/tmp/r1_tts_16k.wav";
|
||||
|
||||
if (!synthesize(
|
||||
text,
|
||||
piper_wav)) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Piper's Czech model produces 22050 Hz.
|
||||
*
|
||||
* R1 PlayStream requires 16000 Hz mono PCM.
|
||||
*
|
||||
* Resample with ffmpeg.
|
||||
*/
|
||||
std::ostringstream resample;
|
||||
|
||||
resample
|
||||
<< "ffmpeg -y -loglevel error"
|
||||
<< " -i "
|
||||
<< '\''
|
||||
<< piper_wav
|
||||
<< '\''
|
||||
<< " -ar 16000"
|
||||
<< " -ac 1"
|
||||
<< " -c:a pcm_s16le"
|
||||
<< " "
|
||||
<< '\''
|
||||
<< output_wav_16k
|
||||
<< '\''
|
||||
<< " >/dev/null 2>&1";
|
||||
|
||||
if (!runCommand(
|
||||
resample.str())) {
|
||||
|
||||
std::cerr
|
||||
<< "ffmpeg resampling failed\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> pcm;
|
||||
|
||||
uint32_t sample_rate = 0;
|
||||
uint16_t channels = 0;
|
||||
uint16_t bits = 0;
|
||||
|
||||
if (!readWavPcm(
|
||||
output_wav_16k,
|
||||
pcm,
|
||||
sample_rate,
|
||||
channels,
|
||||
bits)) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout
|
||||
<< "TTS WAV: "
|
||||
<< sample_rate
|
||||
<< " Hz, "
|
||||
<< channels
|
||||
<< " channel, "
|
||||
<< bits
|
||||
<< " bit, "
|
||||
<< pcm.size()
|
||||
<< " PCM bytes\n";
|
||||
|
||||
if (sample_rate != 16000 ||
|
||||
channels != 1 ||
|
||||
bits != 16) {
|
||||
|
||||
std::cerr
|
||||
<< "Resampled WAV has unexpected format\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return playPcm(pcm);
|
||||
}
|
||||
|
||||
} // namespace r1_voice
|
||||
@@ -0,0 +1,54 @@
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
#include <unitree/robot/go2/video/video_client.hpp>
|
||||
#include <unitree/robot/channel/channel_factory.hpp>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::cerr << "Usage: vision_node <network_interface>\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string frame_path = "/dev/shm/robot_frame.jpg";
|
||||
const std::string temp_path = "/dev/shm/robot_frame.tmp.jpg";
|
||||
|
||||
unitree::robot::ChannelFactory::Instance()->Init(0, argv[1]);
|
||||
unitree::robot::go2::VideoClient video;
|
||||
video.SetTimeout(1.0f);
|
||||
video.Init();
|
||||
|
||||
std::cout << "[SYS] Writing camera frames to " << frame_path << std::endl;
|
||||
|
||||
std::vector<uint8_t> image;
|
||||
while (true) {
|
||||
const int ret = video.GetImageSample(image);
|
||||
if (ret != 0 || image.empty()) {
|
||||
std::cerr << "[WARN] GetImageSample failed: " << ret << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(33));
|
||||
continue;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
{
|
||||
std::ofstream f(temp_path, std::ios::binary | std::ios::trunc);
|
||||
if (f) {
|
||||
f.write(reinterpret_cast<const char*>(image.data()),
|
||||
static_cast<std::streamsize>(image.size()));
|
||||
f.flush();
|
||||
ok = f.good();
|
||||
}
|
||||
}
|
||||
|
||||
if (ok && std::rename(temp_path.c_str(), frame_path.c_str()) != 0) {
|
||||
std::perror("[ERROR] rename");
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(33));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "r1_voice/conversation.hh"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
static void printUsage(
|
||||
const char* program)
|
||||
{
|
||||
std::cerr
|
||||
<< "Usage:\n\n"
|
||||
<< " "
|
||||
<< program
|
||||
<< " <network_interface> "
|
||||
<< "[whisper_cli] "
|
||||
<< "[whisper_model] "
|
||||
<< "[llm_url]\n\n"
|
||||
|
||||
<< "Example:\n\n"
|
||||
|
||||
<< " "
|
||||
<< program
|
||||
<< " enp3s0 "
|
||||
<< "/home/jvizner/esarobotech/whisper.cpp/build/bin/whisper-cli "
|
||||
<< "/home/jvizner/esarobotech/whisper.cpp/models/ggml-medium.bin "
|
||||
<< "http://127.0.0.1:8080/v1/chat/completions\n";
|
||||
}
|
||||
|
||||
int main(
|
||||
int argc,
|
||||
char** argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
printUsage(argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const std::string interface =
|
||||
argv[1];
|
||||
|
||||
r1_voice::ConversationConfig config;
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Whisper STT
|
||||
// ---------------------------------------------------------
|
||||
|
||||
config.stt.whisper_cli =
|
||||
argc >= 3
|
||||
? argv[2]
|
||||
: "/home/jvizner/esarobotech/whisper.cpp/build/bin/whisper-cli";
|
||||
|
||||
config.stt.whisper_model =
|
||||
argc >= 4
|
||||
? argv[3]
|
||||
: "/home/jvizner/esarobotech/whisper.cpp/models/ggml-medium.bin";
|
||||
|
||||
// Czech speech
|
||||
config.stt.language = "cs";
|
||||
|
||||
config.stt.threads = 8;
|
||||
|
||||
// Better protection against clipping the first words.
|
||||
config.stt.speech_threshold = 0.010f;
|
||||
config.stt.preroll_ms = 800;
|
||||
|
||||
config.stt.min_speech_ms = 250;
|
||||
config.stt.silence_ms = 900;
|
||||
config.stt.max_record_ms = 10000;
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Qwen LLM
|
||||
// ---------------------------------------------------------
|
||||
|
||||
config.llm.url =
|
||||
argc >= 5
|
||||
? argv[4]
|
||||
: "http://127.0.0.1:8080/v1/chat/completions";
|
||||
|
||||
config.llm.model = "local";
|
||||
|
||||
config.llm.temperature = 0.4f;
|
||||
config.llm.max_tokens = 80;
|
||||
|
||||
config.llm.system_prompt =
|
||||
"Jsi R1, humanoidní robot. "
|
||||
"Mluv česky, pokud uživatel mluví česky. "
|
||||
"Odpovídej stručně a přirozeně, obvykle jednou až dvěma větami. "
|
||||
"Pokud je vstup zjevně nesrozumitelný nebo poškozený rozpoznáváním řeči, "
|
||||
"nevymýšlej jeho význam a jednoduše požádej uživatele, aby větu zopakoval. "
|
||||
"Neopakuj stále stejnou odpověď. "
|
||||
"Nepoužívej Markdown. "
|
||||
"Nevypisuj uvažování ani <think> bloky.";
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Czech Piper TTS
|
||||
// ---------------------------------------------------------
|
||||
|
||||
config.tts.model =
|
||||
"/home/jvizner/esarobotech/piper/cs_medium.onnx";
|
||||
|
||||
config.tts.output_wav =
|
||||
"/tmp/r1_tts.wav";
|
||||
|
||||
config.tts.volume = 100;
|
||||
|
||||
config.tts.app_name =
|
||||
"example";
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Conversation
|
||||
// ---------------------------------------------------------
|
||||
|
||||
r1_voice::Conversation conversation(
|
||||
config
|
||||
);
|
||||
|
||||
if (!conversation.init(interface)) {
|
||||
std::cerr
|
||||
<< "Failed to initialize voice system\n";
|
||||
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
conversation.run();
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Reference in New Issue
Block a user