TtsClient.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. // 腾讯云语音合成 (TTS) - TextToVoice 轻量 C++ 客户端 实现
  2. //
  3. // 签名算法:TC3-HMAC-SHA256
  4. // 参考:https://cloud.tencent.com/document/api/213/30654
  5. // 已用官方示例向量校验:HashedRequestPayload 与 HashedCanonicalRequest 与文档中间值一致。
  6. #include "pch.h"
  7. #include "TtsClient.h"
  8. #include <curl/curl.h>
  9. #include <cctype>
  10. #include <cstdio>
  11. #include <cstdlib>
  12. #include <cstring>
  13. #include <ctime>
  14. #include <fstream>
  15. #include <random>
  16. #include <sstream>
  17. // 加密统一走 OpenSSL(见 CryptoUtil.h):
  18. // - Win7 / Win10 / Linux 同一套实现,不再维护 BCrypt / OpenSSL 两套分支
  19. // - 顺带少链一个 bcrypt.lib
  20. #include "CryptoUtil.h"
  21. #ifdef _WIN32
  22. #ifndef WIN32_LEAN_AND_MEAN
  23. #define WIN32_LEAN_AND_MEAN
  24. #endif
  25. #include <windows.h>
  26. #endif
  27. namespace txcloud {
  28. namespace {
  29. // ---------------- 基础工具 ----------------
  30. std::string HexEncode(const uint8_t* data, size_t len) {
  31. static const char* kHex = "0123456789abcdef";
  32. std::string out;
  33. out.reserve(len * 2);
  34. for (size_t i = 0; i < len; ++i) {
  35. out.push_back(kHex[(data[i] >> 4) & 0x0F]);
  36. out.push_back(kHex[data[i] & 0x0F]);
  37. }
  38. return out;
  39. }
  40. // HMAC-SHA256(key, msg),key 为前一次 HMAC 的原始字节
  41. std::vector<uint8_t> HmacSha256Chain(const std::vector<uint8_t>& key, const std::string& msg) {
  42. return txcloud::HmacSha256Raw(key.data(), key.size(), msg);
  43. }
  44. std::string JsonEscape(const std::string& s) {
  45. std::string out;
  46. out.reserve(s.size() + 16);
  47. for (uint8_t c : s) {
  48. switch (c) {
  49. case '"': out += "\\\""; break;
  50. case '\\': out += "\\\\"; break;
  51. case '\n': out += "\\n"; break;
  52. case '\r': out += "\\r"; break;
  53. case '\t': out += "\\t"; break;
  54. default:
  55. if (c < 0x20) {
  56. char buf[8];
  57. std::snprintf(buf, sizeof(buf), "\\u%04x", c);
  58. out += buf;
  59. } else {
  60. out.push_back((char)c); // UTF-8 原样透传
  61. }
  62. }
  63. }
  64. return out;
  65. }
  66. // 从 JSON 中取出 "key": "value" 的字符串值(不做完整解析,够用且无依赖)
  67. bool JsonGetString(const std::string& json, const std::string& key, std::string* value) {
  68. std::string needle = "\"" + key + "\"";
  69. size_t pos = json.find(needle);
  70. if (pos == std::string::npos) return false;
  71. pos = json.find(':', pos + needle.size());
  72. if (pos == std::string::npos) return false;
  73. pos = json.find('"', pos);
  74. if (pos == std::string::npos) return false;
  75. ++pos;
  76. std::string out;
  77. for (size_t i = pos; i < json.size(); ++i) {
  78. char c = json[i];
  79. if (c == '\\') {
  80. if (i + 1 < json.size()) {
  81. char n = json[i + 1];
  82. if (n == 'n') out.push_back('\n');
  83. else if (n == 't') out.push_back('\t');
  84. else if (n == 'r') out.push_back('\r');
  85. else if (n == 'u') {
  86. // \uXXXX:简单转成 UTF-8(BMP 范围足够覆盖错误信息里的中英文)
  87. if (i + 5 < json.size()) {
  88. unsigned code = 0;
  89. std::sscanf(json.substr(i + 2, 4).c_str(), "%4x", &code);
  90. i += 4;
  91. if (code < 0x80) {
  92. out.push_back((char)code);
  93. } else if (code < 0x800) {
  94. out.push_back((char)(0xC0 | (code >> 6)));
  95. out.push_back((char)(0x80 | (code & 0x3F)));
  96. } else {
  97. out.push_back((char)(0xE0 | (code >> 12)));
  98. out.push_back((char)(0x80 | ((code >> 6) & 0x3F)));
  99. out.push_back((char)(0x80 | (code & 0x3F)));
  100. }
  101. }
  102. }
  103. else out.push_back(n);
  104. ++i;
  105. continue;
  106. }
  107. }
  108. if (c == '"') {
  109. *value = out;
  110. return true;
  111. }
  112. out.push_back(c);
  113. }
  114. return false;
  115. }
  116. std::string FormatFloat(float v) {
  117. char buf[32];
  118. std::snprintf(buf, sizeof(buf), "%.2f", (double)v);
  119. return buf;
  120. }
  121. std::string MakeSessionId() {
  122. static std::mt19937 rng((unsigned)std::time(nullptr));
  123. char buf[64];
  124. std::snprintf(buf, sizeof(buf), "%llx%08x%08x",
  125. (unsigned long long)std::time(nullptr), (unsigned)rng(), (unsigned)rng());
  126. return std::string(buf);
  127. }
  128. // RFC1123 风格的 UTC 日期(YYYY-MM-DD)
  129. std::string UtcDate(time_t t) {
  130. char buf[16];
  131. #ifdef _WIN32
  132. tm tm_val{};
  133. gmtime_s(&tm_val, &t);
  134. std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tm_val);
  135. #else
  136. tm tm_val{};
  137. gmtime_r(&t, &tm_val);
  138. std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tm_val);
  139. #endif
  140. return buf;
  141. }
  142. size_t CurlWriteCallback(void* ptr, size_t size, size_t nmemb, void* userdata) {
  143. size_t total = size * nmemb;
  144. std::string* s = static_cast<std::string*>(userdata);
  145. s->append(static_cast<char*>(ptr), total);
  146. return total;
  147. }
  148. // 找到 WAV 中 data 块的起始偏移,便于拼接时跳过文件头
  149. size_t FindWavDataOffset(const std::vector<uint8_t>& wav) {
  150. if (wav.size() < 44) return 0;
  151. if (std::memcmp(wav.data(), "RIFF", 4) != 0 ||
  152. std::memcmp(wav.data() + 8, "WAVE", 4) != 0) {
  153. return 0;
  154. }
  155. size_t pos = 12;
  156. while (pos + 8 <= wav.size()) {
  157. uint32_t chunk_size = 0;
  158. std::memcpy(&chunk_size, wav.data() + pos + 4, 4);
  159. if (std::memcmp(wav.data() + pos, "data", 4) == 0) return pos + 8;
  160. pos += 8 + chunk_size + (chunk_size % 2);
  161. }
  162. return 0;
  163. }
  164. // 按标点把 UTF-8 文本切成若干段,每段字符数 <= max_chars
  165. std::vector<std::string> SplitText(const std::string& text, int max_chars) {
  166. static const char* kAsciiPunct = "!?;\n";
  167. static const char* kCjkPunct = "\xe3\x80\x82\xef\xbc\x81\xef\xbc\x9f\xef\xbc\x9b"; // 。!?;
  168. std::vector<std::string> segs;
  169. std::string cur;
  170. int count = 0;
  171. size_t i = 0;
  172. while (i < text.size()) {
  173. size_t len = 1;
  174. uint8_t c = (uint8_t)text[i];
  175. if (c >= 0xF0) len = 4;
  176. else if (c >= 0xE0) len = 3;
  177. else if (c >= 0xC0) len = 2;
  178. std::string ch = text.substr(i, len);
  179. cur += ch;
  180. ++count;
  181. bool is_punct = (len == 1)
  182. ? (std::strchr(kAsciiPunct, (int)c) != nullptr)
  183. : (std::string(kCjkPunct).find(ch) != std::string::npos);
  184. if (is_punct && count >= max_chars / 2) {
  185. segs.push_back(cur);
  186. cur.clear();
  187. count = 0;
  188. } else if (count >= max_chars) {
  189. // 没碰到标点也强制断开
  190. segs.push_back(cur);
  191. cur.clear();
  192. count = 0;
  193. }
  194. i += len;
  195. }
  196. if (!cur.empty()) segs.push_back(cur);
  197. return segs;
  198. }
  199. const char* kHost = "tts.tencentcloudapi.com";
  200. const char* kService = "tts";
  201. const char* kAction = "TextToVoice";
  202. const char* kVersion = "2019-08-23";
  203. } // namespace
  204. // ---------------- TtsClient ----------------
  205. TtsClient::TtsClient(std::string secret_id, std::string secret_key)
  206. : secret_id_(std::move(secret_id)), secret_key_(std::move(secret_key)) {
  207. curl_global_init(CURL_GLOBAL_DEFAULT);
  208. }
  209. void TtsClient::SetTimeoutSec(long connect_sec, long total_sec) {
  210. connect_timeout_sec_ = connect_sec > 0 ? connect_sec : 10;
  211. total_timeout_sec_ = total_sec > 0 ? total_sec : 30;
  212. }
  213. TtsResult TtsClient::Synthesize(const std::string& text_utf8, const TtsOptions& opt) {
  214. TtsResult result;
  215. // 1) 组织请求体(字段名必须与文档一致)
  216. std::ostringstream body;
  217. body << "{";
  218. body << "\"Text\":\"" << JsonEscape(text_utf8) << "\"";
  219. body << ",\"SessionId\":\"" << MakeSessionId() << "\"";
  220. body << ",\"ModelType\":" << opt.model_type;
  221. body << ",\"VoiceType\":" << opt.voice_type;
  222. body << ",\"Volume\":" << FormatFloat(opt.volume);
  223. body << ",\"Speed\":" << FormatFloat(opt.speed);
  224. body << ",\"SampleRate\":" << opt.sample_rate;
  225. body << ",\"Codec\":\"" << opt.codec << "\"";
  226. if (!opt.emotion_category.empty()) {
  227. body << ",\"EmotionCategory\":\"" << opt.emotion_category << "\"";
  228. body << ",\"EmotionIntensity\":" << opt.emotion_intensity;
  229. }
  230. body << ",\"EnableSubtitle\":" << (opt.enable_subtitle ? "true" : "false");
  231. body << "}";
  232. const std::string payload = body.str();
  233. // 2) TC3-HMAC-SHA256 签名
  234. time_t now = std::time(nullptr);
  235. const std::string date = UtcDate(now);
  236. const std::string timestamp = std::to_string((long long)now);
  237. const std::string canonical_headers =
  238. std::string("content-type:application/json; charset=utf-8\n") +
  239. "host:" + kHost + "\n" +
  240. "x-tc-action:" + [] {
  241. std::string a = kAction;
  242. for (char& c : a) c = (char)::tolower((unsigned char)c);
  243. return a;
  244. }() + "\n";
  245. const std::string signed_headers = "content-type;host;x-tc-action";
  246. const std::string canonical_request =
  247. std::string("POST\n/\n\n") + canonical_headers + "\n" +
  248. signed_headers + "\n" + txcloud::Sha256Hex(payload);
  249. const std::string credential_scope = date + "/" + kService + "/tc3_request";
  250. const std::string string_to_sign =
  251. std::string("TC3-HMAC-SHA256\n") + timestamp + "\n" +
  252. credential_scope + "\n" + txcloud::Sha256Hex(canonical_request);
  253. std::vector<uint8_t> secret_date =
  254. txcloud::HmacSha256Raw(("TC3" + secret_key_).data(), secret_key_.size() + 3, date);
  255. std::vector<uint8_t> secret_service = HmacSha256Chain(secret_date, kService);
  256. std::vector<uint8_t> secret_signing = HmacSha256Chain(secret_service, "tc3_request");
  257. std::vector<uint8_t> signature_raw = HmacSha256Chain(secret_signing, string_to_sign);
  258. const std::string signature = HexEncode(signature_raw.data(), signature_raw.size());
  259. const std::string authorization =
  260. std::string("TC3-HMAC-SHA256 Credential=") + secret_id_ + "/" + credential_scope +
  261. ", SignedHeaders=" + signed_headers + ", Signature=" + signature;
  262. // 3) 发请求
  263. CURL* curl = curl_easy_init();
  264. if (!curl) {
  265. result.error_message = "curl_easy_init failed";
  266. return result;
  267. }
  268. std::string response;
  269. struct curl_slist* headers = nullptr;
  270. headers = curl_slist_append(headers, "Content-Type: application/json; charset=utf-8");
  271. headers = curl_slist_append(headers, (std::string("Host: ") + kHost).c_str());
  272. headers = curl_slist_append(headers, (std::string("X-TC-Action: ") + kAction).c_str());
  273. headers = curl_slist_append(headers, (std::string("X-TC-Version: ") + kVersion).c_str());
  274. headers = curl_slist_append(headers, (std::string("X-TC-Timestamp: ") + timestamp).c_str());
  275. headers = curl_slist_append(headers, (std::string("X-TC-Region: ") + opt.region).c_str());
  276. headers = curl_slist_append(headers, (std::string("Authorization: ") + authorization).c_str());
  277. curl_easy_setopt(curl, CURLOPT_URL, (std::string("https://") + kHost + "/").c_str());
  278. curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
  279. curl_easy_setopt(curl, CURLOPT_POST, 1L);
  280. curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str());
  281. curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)payload.size());
  282. curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
  283. curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
  284. curl_easy_setopt(curl, CURLOPT_TIMEOUT, total_timeout_sec_);
  285. curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, connect_timeout_sec_);
  286. // ---- TLS ----
  287. // Win7 关键:SChannel 默认不启用 TLS 1.2,这里显式锁死下限,避免被降级到 TLS 1.0
  288. // (腾讯云 API 只接受 TLS 1.2+)。CURL_SSLVERSION_TLSv1_2 需要 curl >= 7.34.0。
  289. #if LIBCURL_VERSION_NUM >= 0x072200
  290. curl_easy_setopt(curl, CURLOPT_SSLVERSION, (long)CURL_SSLVERSION_TLSv1_2);
  291. #endif
  292. curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
  293. if (verify_peer_) {
  294. curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
  295. if (!ca_file_.empty()) {
  296. curl_easy_setopt(curl, CURLOPT_CAINFO, ca_file_.c_str());
  297. } else {
  298. // Windows + curl >= 7.71:直接用操作系统自带的证书库。
  299. // 这是最省事也最安全的做法 —— OpenSSL 后端在 Windows 上默认找不到
  300. // OpenSSL 自己目录下的根证书,不设这个就必定报
  301. // "SSL peer certificate or SSH remote key was not OK"。
  302. // 有了它就不用自己下载维护 cacert.pem,安全性也不打折。
  303. #if defined(_WIN32) && LIBCURL_VERSION_NUM >= 0x074700
  304. curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NATIVE_CA);
  305. #endif
  306. }
  307. } else {
  308. curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
  309. }
  310. CURLcode rc = curl_easy_perform(curl);
  311. long http_code = 0;
  312. curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
  313. curl_slist_free_all(headers);
  314. curl_easy_cleanup(curl);
  315. result.http_code = http_code;
  316. last_response_ = response;
  317. if (rc != CURLE_OK) {
  318. std::string msg = std::string("network error: ") + curl_easy_strerror(rc);
  319. // 证书校验失败是最常见的配置问题,给出明确的下一步而不是干巴巴的英文
  320. if (rc == CURLE_PEER_FAILED_VERIFICATION) {
  321. msg += "(提示:libcurl 找不到/校验不通过服务端证书。";
  322. if (verify_peer_) {
  323. if (ca_file_.empty()) {
  324. msg += "可下载 https://curl.se/ca/cacert.pem,设环境变量 TTS_CA_FILE;"
  325. "或调 client.SetCaFile();或临时 SetVerifyPeer(false) 跳过校验。)";
  326. } else {
  327. msg += "已设置 CA 文件但仍失败,请确认路径正确、是 PEM 格式且未被防火墙篡改。)";
  328. }
  329. } else {
  330. msg += "已禁用证书校验仍失败,问题不在证书,可能是 TLS 1.2 不支持:见 README 9.2。)";
  331. }
  332. }
  333. result.error_message = msg;
  334. return result;
  335. }
  336. // 4) 解析响应
  337. JsonGetString(response, "RequestId", &result.request_id);
  338. std::string err_code, err_msg;
  339. size_t err_pos = response.find("\"Error\"");
  340. if (err_pos != std::string::npos) {
  341. std::string tail = response.substr(err_pos, 1024);
  342. JsonGetString(tail, "Code", &err_code);
  343. JsonGetString(tail, "Message", &err_msg);
  344. }
  345. std::string audio_b64;
  346. if (JsonGetString(response, "Audio", &audio_b64) && !audio_b64.empty()) {
  347. std::string raw = txcloud::Base64DecodeBytes(audio_b64);
  348. result.audio.assign(raw.begin(), raw.end());
  349. result.ok = !result.audio.empty();
  350. if (!result.ok) result.error_message = "audio decode failed";
  351. } else {
  352. result.error_code = err_code.empty() ? "Unknown" : err_code;
  353. result.error_message = err_msg.empty() ? ("unexpected response: " + response) : err_msg;
  354. }
  355. return result;
  356. }
  357. bool TtsClient::SynthesizeToFile(const std::string& text_utf8, const TtsOptions& opt,
  358. const std::string& out_path, std::string* err) {
  359. TtsResult r = Synthesize(text_utf8, opt);
  360. if (!r.ok) {
  361. if (err) *err = r.error_code + " " + r.error_message;
  362. return false;
  363. }
  364. std::ofstream f(out_path, std::ios::binary);
  365. if (!f) {
  366. if (err) *err = "cannot open output file";
  367. return false;
  368. }
  369. f.write(reinterpret_cast<const char*>(r.audio.data()), (std::streamsize)r.audio.size());
  370. return true;
  371. }
  372. bool TtsClient::SynthesizeLongTextToFile(const std::string& text_utf8, const TtsOptions& opt,
  373. const std::string& out_path, int max_chars,
  374. std::string* err) {
  375. std::vector<std::string> segs = SplitText(text_utf8, max_chars);
  376. std::ofstream f(out_path, std::ios::binary);
  377. if (!f) {
  378. if (err) *err = "cannot open output file";
  379. return false;
  380. }
  381. bool first = true;
  382. for (const std::string& seg : segs) {
  383. TtsResult r = Synthesize(seg, opt);
  384. if (!r.ok) {
  385. if (err) *err = r.error_code + " " + r.error_message;
  386. return false;
  387. }
  388. if (first || opt.codec == "mp3") {
  389. f.write(reinterpret_cast<const char*>(r.audio.data()), (std::streamsize)r.audio.size());
  390. } else if (opt.codec == "wav") {
  391. size_t off = FindWavDataOffset(r.audio);
  392. f.write(reinterpret_cast<const char*>(r.audio.data() + off),
  393. (std::streamsize)(r.audio.size() - off));
  394. } else { // pcm 无文件头,直接追加
  395. f.write(reinterpret_cast<const char*>(r.audio.data()), (std::streamsize)r.audio.size());
  396. }
  397. first = false;
  398. }
  399. return true;
  400. }
  401. } // namespace txcloud