| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- // 腾讯云语音合成 (TTS) - TextToVoice 轻量 C++ 客户端 实现
- //
- // 签名算法:TC3-HMAC-SHA256
- // 参考:https://cloud.tencent.com/document/api/213/30654
- // 已用官方示例向量校验:HashedRequestPayload 与 HashedCanonicalRequest 与文档中间值一致。
- #include "pch.h"
- #include "TtsClient.h"
- #include <curl/curl.h>
- #include <cctype>
- #include <cstdio>
- #include <cstdlib>
- #include <cstring>
- #include <ctime>
- #include <fstream>
- #include <random>
- #include <sstream>
- // 加密统一走 OpenSSL(见 CryptoUtil.h):
- // - Win7 / Win10 / Linux 同一套实现,不再维护 BCrypt / OpenSSL 两套分支
- // - 顺带少链一个 bcrypt.lib
- #include "CryptoUtil.h"
- #ifdef _WIN32
- #ifndef WIN32_LEAN_AND_MEAN
- #define WIN32_LEAN_AND_MEAN
- #endif
- #include <windows.h>
- #endif
- namespace txcloud {
- namespace {
- // ---------------- 基础工具 ----------------
- std::string HexEncode(const uint8_t* data, size_t len) {
- static const char* kHex = "0123456789abcdef";
- std::string out;
- out.reserve(len * 2);
- for (size_t i = 0; i < len; ++i) {
- out.push_back(kHex[(data[i] >> 4) & 0x0F]);
- out.push_back(kHex[data[i] & 0x0F]);
- }
- return out;
- }
- // HMAC-SHA256(key, msg),key 为前一次 HMAC 的原始字节
- std::vector<uint8_t> HmacSha256Chain(const std::vector<uint8_t>& key, const std::string& msg) {
- return txcloud::HmacSha256Raw(key.data(), key.size(), msg);
- }
- std::string JsonEscape(const std::string& s) {
- std::string out;
- out.reserve(s.size() + 16);
- for (uint8_t c : s) {
- switch (c) {
- case '"': out += "\\\""; break;
- case '\\': out += "\\\\"; break;
- case '\n': out += "\\n"; break;
- case '\r': out += "\\r"; break;
- case '\t': out += "\\t"; break;
- default:
- if (c < 0x20) {
- char buf[8];
- std::snprintf(buf, sizeof(buf), "\\u%04x", c);
- out += buf;
- } else {
- out.push_back((char)c); // UTF-8 原样透传
- }
- }
- }
- return out;
- }
- // 从 JSON 中取出 "key": "value" 的字符串值(不做完整解析,够用且无依赖)
- bool JsonGetString(const std::string& json, const std::string& key, std::string* value) {
- std::string needle = "\"" + key + "\"";
- size_t pos = json.find(needle);
- if (pos == std::string::npos) return false;
- pos = json.find(':', pos + needle.size());
- if (pos == std::string::npos) return false;
- pos = json.find('"', pos);
- if (pos == std::string::npos) return false;
- ++pos;
- std::string out;
- for (size_t i = pos; i < json.size(); ++i) {
- char c = json[i];
- if (c == '\\') {
- if (i + 1 < json.size()) {
- char n = json[i + 1];
- if (n == 'n') out.push_back('\n');
- else if (n == 't') out.push_back('\t');
- else if (n == 'r') out.push_back('\r');
- else if (n == 'u') {
- // \uXXXX:简单转成 UTF-8(BMP 范围足够覆盖错误信息里的中英文)
- if (i + 5 < json.size()) {
- unsigned code = 0;
- std::sscanf(json.substr(i + 2, 4).c_str(), "%4x", &code);
- i += 4;
- if (code < 0x80) {
- out.push_back((char)code);
- } else if (code < 0x800) {
- out.push_back((char)(0xC0 | (code >> 6)));
- out.push_back((char)(0x80 | (code & 0x3F)));
- } else {
- out.push_back((char)(0xE0 | (code >> 12)));
- out.push_back((char)(0x80 | ((code >> 6) & 0x3F)));
- out.push_back((char)(0x80 | (code & 0x3F)));
- }
- }
- }
- else out.push_back(n);
- ++i;
- continue;
- }
- }
- if (c == '"') {
- *value = out;
- return true;
- }
- out.push_back(c);
- }
- return false;
- }
- std::string FormatFloat(float v) {
- char buf[32];
- std::snprintf(buf, sizeof(buf), "%.2f", (double)v);
- return buf;
- }
- std::string MakeSessionId() {
- static std::mt19937 rng((unsigned)std::time(nullptr));
- char buf[64];
- std::snprintf(buf, sizeof(buf), "%llx%08x%08x",
- (unsigned long long)std::time(nullptr), (unsigned)rng(), (unsigned)rng());
- return std::string(buf);
- }
- // RFC1123 风格的 UTC 日期(YYYY-MM-DD)
- std::string UtcDate(time_t t) {
- char buf[16];
- #ifdef _WIN32
- tm tm_val{};
- gmtime_s(&tm_val, &t);
- std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tm_val);
- #else
- tm tm_val{};
- gmtime_r(&t, &tm_val);
- std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tm_val);
- #endif
- return buf;
- }
- size_t CurlWriteCallback(void* ptr, size_t size, size_t nmemb, void* userdata) {
- size_t total = size * nmemb;
- std::string* s = static_cast<std::string*>(userdata);
- s->append(static_cast<char*>(ptr), total);
- return total;
- }
- // 找到 WAV 中 data 块的起始偏移,便于拼接时跳过文件头
- size_t FindWavDataOffset(const std::vector<uint8_t>& wav) {
- if (wav.size() < 44) return 0;
- if (std::memcmp(wav.data(), "RIFF", 4) != 0 ||
- std::memcmp(wav.data() + 8, "WAVE", 4) != 0) {
- return 0;
- }
- size_t pos = 12;
- while (pos + 8 <= wav.size()) {
- uint32_t chunk_size = 0;
- std::memcpy(&chunk_size, wav.data() + pos + 4, 4);
- if (std::memcmp(wav.data() + pos, "data", 4) == 0) return pos + 8;
- pos += 8 + chunk_size + (chunk_size % 2);
- }
- return 0;
- }
- // 按标点把 UTF-8 文本切成若干段,每段字符数 <= max_chars
- std::vector<std::string> SplitText(const std::string& text, int max_chars) {
- static const char* kAsciiPunct = "!?;\n";
- static const char* kCjkPunct = "\xe3\x80\x82\xef\xbc\x81\xef\xbc\x9f\xef\xbc\x9b"; // 。!?;
- std::vector<std::string> segs;
- std::string cur;
- int count = 0;
- size_t i = 0;
- while (i < text.size()) {
- size_t len = 1;
- uint8_t c = (uint8_t)text[i];
- if (c >= 0xF0) len = 4;
- else if (c >= 0xE0) len = 3;
- else if (c >= 0xC0) len = 2;
- std::string ch = text.substr(i, len);
- cur += ch;
- ++count;
- bool is_punct = (len == 1)
- ? (std::strchr(kAsciiPunct, (int)c) != nullptr)
- : (std::string(kCjkPunct).find(ch) != std::string::npos);
- if (is_punct && count >= max_chars / 2) {
- segs.push_back(cur);
- cur.clear();
- count = 0;
- } else if (count >= max_chars) {
- // 没碰到标点也强制断开
- segs.push_back(cur);
- cur.clear();
- count = 0;
- }
- i += len;
- }
- if (!cur.empty()) segs.push_back(cur);
- return segs;
- }
- const char* kHost = "tts.tencentcloudapi.com";
- const char* kService = "tts";
- const char* kAction = "TextToVoice";
- const char* kVersion = "2019-08-23";
- } // namespace
- // ---------------- TtsClient ----------------
- TtsClient::TtsClient(std::string secret_id, std::string secret_key)
- : secret_id_(std::move(secret_id)), secret_key_(std::move(secret_key)) {
- curl_global_init(CURL_GLOBAL_DEFAULT);
- }
- void TtsClient::SetTimeoutSec(long connect_sec, long total_sec) {
- connect_timeout_sec_ = connect_sec > 0 ? connect_sec : 10;
- total_timeout_sec_ = total_sec > 0 ? total_sec : 30;
- }
- TtsResult TtsClient::Synthesize(const std::string& text_utf8, const TtsOptions& opt) {
- TtsResult result;
- // 1) 组织请求体(字段名必须与文档一致)
- std::ostringstream body;
- body << "{";
- body << "\"Text\":\"" << JsonEscape(text_utf8) << "\"";
- body << ",\"SessionId\":\"" << MakeSessionId() << "\"";
- body << ",\"ModelType\":" << opt.model_type;
- body << ",\"VoiceType\":" << opt.voice_type;
- body << ",\"Volume\":" << FormatFloat(opt.volume);
- body << ",\"Speed\":" << FormatFloat(opt.speed);
- body << ",\"SampleRate\":" << opt.sample_rate;
- body << ",\"Codec\":\"" << opt.codec << "\"";
- if (!opt.emotion_category.empty()) {
- body << ",\"EmotionCategory\":\"" << opt.emotion_category << "\"";
- body << ",\"EmotionIntensity\":" << opt.emotion_intensity;
- }
- body << ",\"EnableSubtitle\":" << (opt.enable_subtitle ? "true" : "false");
- body << "}";
- const std::string payload = body.str();
- // 2) TC3-HMAC-SHA256 签名
- time_t now = std::time(nullptr);
- const std::string date = UtcDate(now);
- const std::string timestamp = std::to_string((long long)now);
- const std::string canonical_headers =
- std::string("content-type:application/json; charset=utf-8\n") +
- "host:" + kHost + "\n" +
- "x-tc-action:" + [] {
- std::string a = kAction;
- for (char& c : a) c = (char)::tolower((unsigned char)c);
- return a;
- }() + "\n";
- const std::string signed_headers = "content-type;host;x-tc-action";
- const std::string canonical_request =
- std::string("POST\n/\n\n") + canonical_headers + "\n" +
- signed_headers + "\n" + txcloud::Sha256Hex(payload);
- const std::string credential_scope = date + "/" + kService + "/tc3_request";
- const std::string string_to_sign =
- std::string("TC3-HMAC-SHA256\n") + timestamp + "\n" +
- credential_scope + "\n" + txcloud::Sha256Hex(canonical_request);
- std::vector<uint8_t> secret_date =
- txcloud::HmacSha256Raw(("TC3" + secret_key_).data(), secret_key_.size() + 3, date);
- std::vector<uint8_t> secret_service = HmacSha256Chain(secret_date, kService);
- std::vector<uint8_t> secret_signing = HmacSha256Chain(secret_service, "tc3_request");
- std::vector<uint8_t> signature_raw = HmacSha256Chain(secret_signing, string_to_sign);
- const std::string signature = HexEncode(signature_raw.data(), signature_raw.size());
- const std::string authorization =
- std::string("TC3-HMAC-SHA256 Credential=") + secret_id_ + "/" + credential_scope +
- ", SignedHeaders=" + signed_headers + ", Signature=" + signature;
- // 3) 发请求
- CURL* curl = curl_easy_init();
- if (!curl) {
- result.error_message = "curl_easy_init failed";
- return result;
- }
- std::string response;
- struct curl_slist* headers = nullptr;
- headers = curl_slist_append(headers, "Content-Type: application/json; charset=utf-8");
- headers = curl_slist_append(headers, (std::string("Host: ") + kHost).c_str());
- headers = curl_slist_append(headers, (std::string("X-TC-Action: ") + kAction).c_str());
- headers = curl_slist_append(headers, (std::string("X-TC-Version: ") + kVersion).c_str());
- headers = curl_slist_append(headers, (std::string("X-TC-Timestamp: ") + timestamp).c_str());
- headers = curl_slist_append(headers, (std::string("X-TC-Region: ") + opt.region).c_str());
- headers = curl_slist_append(headers, (std::string("Authorization: ") + authorization).c_str());
- curl_easy_setopt(curl, CURLOPT_URL, (std::string("https://") + kHost + "/").c_str());
- curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
- curl_easy_setopt(curl, CURLOPT_POST, 1L);
- curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str());
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)payload.size());
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
- curl_easy_setopt(curl, CURLOPT_TIMEOUT, total_timeout_sec_);
- curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, connect_timeout_sec_);
- // ---- TLS ----
- // Win7 关键:SChannel 默认不启用 TLS 1.2,这里显式锁死下限,避免被降级到 TLS 1.0
- // (腾讯云 API 只接受 TLS 1.2+)。CURL_SSLVERSION_TLSv1_2 需要 curl >= 7.34.0。
- #if LIBCURL_VERSION_NUM >= 0x072200
- curl_easy_setopt(curl, CURLOPT_SSLVERSION, (long)CURL_SSLVERSION_TLSv1_2);
- #endif
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
- if (verify_peer_) {
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
- if (!ca_file_.empty()) {
- curl_easy_setopt(curl, CURLOPT_CAINFO, ca_file_.c_str());
- } else {
- // Windows + curl >= 7.71:直接用操作系统自带的证书库。
- // 这是最省事也最安全的做法 —— OpenSSL 后端在 Windows 上默认找不到
- // OpenSSL 自己目录下的根证书,不设这个就必定报
- // "SSL peer certificate or SSH remote key was not OK"。
- // 有了它就不用自己下载维护 cacert.pem,安全性也不打折。
- #if defined(_WIN32) && LIBCURL_VERSION_NUM >= 0x074700
- curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NATIVE_CA);
- #endif
- }
- } else {
- curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
- }
- CURLcode rc = curl_easy_perform(curl);
- long http_code = 0;
- curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
- curl_slist_free_all(headers);
- curl_easy_cleanup(curl);
- result.http_code = http_code;
- last_response_ = response;
- if (rc != CURLE_OK) {
- std::string msg = std::string("network error: ") + curl_easy_strerror(rc);
- // 证书校验失败是最常见的配置问题,给出明确的下一步而不是干巴巴的英文
- if (rc == CURLE_PEER_FAILED_VERIFICATION) {
- msg += "(提示:libcurl 找不到/校验不通过服务端证书。";
- if (verify_peer_) {
- if (ca_file_.empty()) {
- msg += "可下载 https://curl.se/ca/cacert.pem,设环境变量 TTS_CA_FILE;"
- "或调 client.SetCaFile();或临时 SetVerifyPeer(false) 跳过校验。)";
- } else {
- msg += "已设置 CA 文件但仍失败,请确认路径正确、是 PEM 格式且未被防火墙篡改。)";
- }
- } else {
- msg += "已禁用证书校验仍失败,问题不在证书,可能是 TLS 1.2 不支持:见 README 9.2。)";
- }
- }
- result.error_message = msg;
- return result;
- }
- // 4) 解析响应
- JsonGetString(response, "RequestId", &result.request_id);
- std::string err_code, err_msg;
- size_t err_pos = response.find("\"Error\"");
- if (err_pos != std::string::npos) {
- std::string tail = response.substr(err_pos, 1024);
- JsonGetString(tail, "Code", &err_code);
- JsonGetString(tail, "Message", &err_msg);
- }
- std::string audio_b64;
- if (JsonGetString(response, "Audio", &audio_b64) && !audio_b64.empty()) {
- std::string raw = txcloud::Base64DecodeBytes(audio_b64);
- result.audio.assign(raw.begin(), raw.end());
- result.ok = !result.audio.empty();
- if (!result.ok) result.error_message = "audio decode failed";
- } else {
- result.error_code = err_code.empty() ? "Unknown" : err_code;
- result.error_message = err_msg.empty() ? ("unexpected response: " + response) : err_msg;
- }
- return result;
- }
- bool TtsClient::SynthesizeToFile(const std::string& text_utf8, const TtsOptions& opt,
- const std::string& out_path, std::string* err) {
- TtsResult r = Synthesize(text_utf8, opt);
- if (!r.ok) {
- if (err) *err = r.error_code + " " + r.error_message;
- return false;
- }
- std::ofstream f(out_path, std::ios::binary);
- if (!f) {
- if (err) *err = "cannot open output file";
- return false;
- }
- f.write(reinterpret_cast<const char*>(r.audio.data()), (std::streamsize)r.audio.size());
- return true;
- }
- bool TtsClient::SynthesizeLongTextToFile(const std::string& text_utf8, const TtsOptions& opt,
- const std::string& out_path, int max_chars,
- std::string* err) {
- std::vector<std::string> segs = SplitText(text_utf8, max_chars);
- std::ofstream f(out_path, std::ios::binary);
- if (!f) {
- if (err) *err = "cannot open output file";
- return false;
- }
- bool first = true;
- for (const std::string& seg : segs) {
- TtsResult r = Synthesize(seg, opt);
- if (!r.ok) {
- if (err) *err = r.error_code + " " + r.error_message;
- return false;
- }
- if (first || opt.codec == "mp3") {
- f.write(reinterpret_cast<const char*>(r.audio.data()), (std::streamsize)r.audio.size());
- } else if (opt.codec == "wav") {
- size_t off = FindWavDataOffset(r.audio);
- f.write(reinterpret_cast<const char*>(r.audio.data() + off),
- (std::streamsize)(r.audio.size() - off));
- } else { // pcm 无文件头,直接追加
- f.write(reinterpret_cast<const char*>(r.audio.data()), (std::streamsize)r.audio.size());
- }
- first = false;
- }
- return true;
- }
- } // namespace txcloud
|