Skip to content

Commit

Permalink
Start building a library for ElevenLabs' API
Browse files Browse the repository at this point in the history
  • Loading branch information
opsnlops committed Jun 2, 2024
1 parent debab7a commit a2ecba0
Show file tree
Hide file tree
Showing 22 changed files with 881 additions and 2 deletions.
9 changes: 7 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25)

project(creature-server
VERSION "2.0.0"
VERSION "2.1.0"
DESCRIPTION "Server for April's Creatures"
HOMEPAGE_URL https://github.com/opsnlops/creature-server
LANGUAGES C CXX)
Expand Down Expand Up @@ -93,8 +93,9 @@ add_definitions(
)


# ...and our own library
# ...and our own libraries
add_subdirectory(lib/e131_service)
add_subdirectory(lib/CreatureVoicesLib)


# Set up a base64 encoder/decoder
Expand Down Expand Up @@ -257,6 +258,8 @@ add_executable(creature-server
src/server/ws/websocket/ClientCafe.cpp
src/server/ws/websocket/ClientConnection.cpp
src/server/ws/messaging/NoticeMessageCommandDTO.h
src/server/ws/service/VoiceService.h
src/server/ws/service/VoiceService.cpp

)

Expand All @@ -277,6 +280,7 @@ target_link_libraries(creature-server
PRIVATE mongo::bsoncxx_shared
argparse
e131_service
CreatureVoicesLib
)


Expand Down Expand Up @@ -358,6 +362,7 @@ target_link_libraries(creature-server-test
PRIVATE mongo::bsoncxx_shared
argparse
e131_service
CreatureVoicesLib
gtest_main
gmock_main
)
Expand Down
97 changes: 97 additions & 0 deletions lib/CreatureVoicesLib/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
cmake_minimum_required(VERSION 3.25)


project(CreatureVoicesLib
VERSION "0.1.0"
DESCRIPTION "Voice library for April's Creatures"
HOMEPAGE_URL https://github.com/opsnlops/creature-server
LANGUAGES C CXX)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 20)

# Overshadowed declarations keep biting me in the butt 😅
add_compile_options(-Wshadow)
add_compile_options(-Wall)
add_compile_options(-Wextra)
add_compile_options(-Wpedantic)

set(PACKAGE_AUTHOR "April White")


# Log loudly
include(FetchContent)
set(FETCHCONTENT_QUIET OFF)

# This project uses threads
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)


find_package(PkgConfig REQUIRED)
find_package(CURL REQUIRED)

# fmt
message(STATUS "fmt")
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt
GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)
set(FMT_HEADER_ONLY ON)
set(FMT_LOCALE ON)


# spdlog
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog
GIT_TAG v1.11.0
)
FetchContent_MakeAvailable(spdlog)


# json
FetchContent_Declare(
json
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz)
FetchContent_MakeAvailable(json)


# Use the oatpp that we've already made
find_package(oatpp REQUIRED PATHS ${CMAKE_SOURCE_DIR}/../../externals/install/lib/cmake/oatpp-1.3.0)

# Define our library
add_library(CreatureVoicesLib STATIC

src/model/HttpMethod.h
src/model/HttpMethod.cpp
src/model/Voice.h
src/model/Voice.cpp


src/CreatureVoices.h
src/CreatureVoices.cpp
src/VoiceResult.h
src/CurlBase.h
src/CurlBase.cpp
src/CurlHandle.h
src/CurlHandle.cpp


)

# Include directories for this library
target_include_directories(CreatureVoicesLib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
${CURL_INCLUDE_DIR}
${oatpp_INCLUDE_DIRS}
)

target_link_libraries(CreatureVoicesLib
${CURL_LIBRARIES}
spdlog::spdlog $<$<BOOL:${MINGW}>:ws2_32>
fmt::fmt
nlohmann_json::nlohmann_json
)
71 changes: 71 additions & 0 deletions lib/CreatureVoicesLib/src/CreatureVoices.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

#include <algorithm>
#include <string>
#include <utility>
#include <vector>

#include <curl/curl.h>
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>


#include "model/HttpMethod.h"
#include "VoiceResult.h"
#include "CreatureVoices.h"

using json = nlohmann::json;

namespace creatures::voice {


CreatureVoices::CreatureVoices(std::string apiKey) : apiKey(std::move(apiKey)) {}


VoiceResult<std::vector<Voice>> CreatureVoices::listAllAvailableVoices() {
const std::string url ="https://api.elevenlabs.io/v1/voices";

debug("Fetching available voices");

auto curlHandle = createCurlHandle(url);
auto res = performRequest(curlHandle, HttpMethod::GET, "");
if(!res.isSuccess()) {
auto error = res.getError();
std::string errorMessage = fmt::format("Failed to fetch available voices: {}", error->getMessage());
curl_easy_cleanup(curlHandle.get());
return VoiceResult<std::vector<Voice>>{VoiceError(error->getCode(), error->getMessage())};
}
curl_easy_cleanup(curlHandle.get());

auto httpResponse = res.getValue().value();
trace("httpResponse was: {}", httpResponse);


json jsonResponse;
try {
jsonResponse = json::parse(httpResponse);
} catch (const json::parse_error& e) {
std::string errorMessage = fmt::format("Failed to parse JSON response: {}", e.what());
warn(errorMessage);
return VoiceResult<std::vector<Voice>>{VoiceError(VoiceError::InvalidData, errorMessage)};
}

std::vector<Voice> voices;
for (const auto& item : jsonResponse["voices"]) {
Voice voice;
voice.voiceId = item["voice_id"].get<std::string>();
voice.name = item["name"].get<std::string>();
voices.push_back(voice);
}
debug("ElevenLabs gave us {} voices", voices.size());


// Sort the voices by name
std::sort(voices.begin(), voices.end(), [](const Voice& a, const Voice& b) {
return a.name < b.name;
});

return voices;
}

}
28 changes: 28 additions & 0 deletions lib/CreatureVoicesLib/src/CreatureVoices.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

#pragma once


#include <curl/curl.h>

#include <string>
#include <vector>

#include "CurlBase.h"
#include "VoiceResult.h"
#include "model/HttpMethod.h"
#include "model/Voice.h"


namespace creatures :: voice {

class CreatureVoices : public CurlBase {
public:
CreatureVoices(std::string apiKey);
VoiceResult<std::vector<Voice>> listAllAvailableVoices();

private:
std::string apiKey;

};

}
128 changes: 128 additions & 0 deletions lib/CreatureVoicesLib/src/CurlBase.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@

#include <algorithm>
#include <string>
#include <vector>

#include <curl/curl.h>
#include <fmt/format.h>
#include <spdlog/spdlog.h>


#include "model/HttpMethod.h"
#include "VoiceResult.h"
#include "CurlBase.h"


namespace creatures::voice {


size_t CurlBase::WriteCallback(char* ptr, size_t size, size_t nmemb, std::string* data) {
data->append(ptr, size * nmemb);
return size * nmemb;
}


CurlHandle CurlBase::createCurlHandle(const std::string& url) {
debug("Creating a curl handle for URL: {}", url);
return CurlHandle(url);
}

VoiceResult<std::string> CurlBase::performRequest(CurlHandle& curlHandle,
HttpMethod method,
const std::string& data) {

debug("performing a request! method: {}, data: {}", httpMethodToString(method), data);

std::string response;
std::string errorMessage;

// Make sure CURL is initialized
if(!curlHandle.get()) {
errorMessage = "Unable to perform request because CURL handle is not initialized";
error(errorMessage);
return VoiceResult<std::string>{VoiceError(VoiceError::InternalError, errorMessage)};
}

curl_easy_setopt(curlHandle.get(), CURLOPT_WRITEDATA, &response);
trace("CURL handle set up for writing");

switch (method) {
case HttpMethod::GET:
// GET is the default method, no need to set anything special
break;
case HttpMethod::POST:
curl_easy_setopt(curlHandle.get(), CURLOPT_POST, 1L);
curl_easy_setopt(curlHandle.get(), CURLOPT_POSTFIELDS, data.c_str());
break;
case HttpMethod::PUT:
curl_easy_setopt(curlHandle.get(), CURLOPT_CUSTOMREQUEST, httpMethodToString(method).c_str());
curl_easy_setopt(curlHandle.get(), CURLOPT_POSTFIELDS, data.c_str());
break;
case HttpMethod::DELETE:
curl_easy_setopt(curlHandle.get(), CURLOPT_CUSTOMREQUEST, httpMethodToString(method).c_str());
break;
default:
errorMessage = fmt::format("Unknown HTTP method: {}", httpMethodToString(method));
error(errorMessage);
curl_easy_cleanup(curlHandle.get());
return VoiceResult<std::string>{VoiceError(VoiceError::InternalError, errorMessage)};
}


CURLcode res = curl_easy_perform(curlHandle.get());
if (res != CURLE_OK) {
errorMessage = fmt::format("CURL request failed: {}", curl_easy_strerror(res));
error(errorMessage);
curl_easy_cleanup(curlHandle.get());
return VoiceResult<std::string>{VoiceError(VoiceError::InternalError, errorMessage)};
}

long http_code = 0;
curl_easy_getinfo(curlHandle.get(), CURLINFO_RESPONSE_CODE, &http_code);

// Let's look at the response code
switch(http_code) {
case 200:
case 201:
case 204:
// These are all good responses
break;
case 301:
case 302:
errorMessage = fmt::format("HTTP error: {} - redirect", http_code);
warn(errorMessage);
curl_easy_cleanup(curlHandle.get());
return VoiceResult<std::string>{VoiceError(VoiceError::InvalidData, errorMessage)};
case 400:
errorMessage = fmt::format("HTTP error: {} - bad request", http_code);
warn(errorMessage);
curl_easy_cleanup(curlHandle.get());
return VoiceResult<std::string>{VoiceError(VoiceError::InvalidData, errorMessage)};
case 401:
case 403:
errorMessage = fmt::format("HTTP error: {} - unauthorized", http_code);
warn(errorMessage);
curl_easy_cleanup(curlHandle.get());
return VoiceResult<std::string>{VoiceError(VoiceError::InvalidApiKey, errorMessage)};
case 404:
errorMessage = fmt::format("HTTP error: {} - not found", http_code);
warn(errorMessage);
curl_easy_cleanup(curlHandle.get());
return VoiceResult<std::string>{VoiceError(VoiceError::NotFound, errorMessage)};

// Map everything else to an error
default:
errorMessage = fmt::format("HTTP error: {}", http_code);
error(errorMessage);
curl_easy_cleanup(curlHandle.get());
return VoiceResult<std::string>{VoiceError(VoiceError::InternalError, errorMessage)};
}


// Looks good! We have good data
curl_easy_cleanup(curlHandle.get());

debug("request successful!");
return VoiceResult<std::string>{response};
}
}
Loading

0 comments on commit a2ecba0

Please sign in to comment.