news 2026/9/28 2:41:22

Googletest 实战 FAQ 深度解析:从命名规则到死亡测试,结合 TEN-framework 的 C++ 测试实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Googletest 实战 FAQ 深度解析:从命名规则到死亡测试,结合 TEN-framework 的 C++ 测试实践
  • 人工智能
  • AI Agent
  • 多模态
  • 语音
  • AI 应用

【免费下载链接】ten-framework

Open-source framework for conversational voice AI agents

项目地址:https://gitcode.com/TEN-framework/ten-framework
点击查看免费下载

导读

本文以 TEN-framework 仓库内置的 googletest 官方 FAQ.md 为骨架,系统梳理 C++ 测试框架 googletest 在实际使用中最容易踩坑的二十余个高频问题——从TEST/TEST_F命名规则、断言宏的 NULL 兼容性、typed tests 与 value-parameterized tests 的选择,到死亡测试(death test)的性能与线程问题、fixture 的继承与生命周期、各类编译期错误的成因与修复。全文以 TEN-framework 仓库中真实存在的测试代码(如 tests/ten_runtime/smoke 下的 smoke 测试、gtest_main.cc 自定义入口等)为佐证,帮助你既理解 googletest 的底层设计意图,又掌握可立即落地的调试与规避技巧。

一、为什么TEST的用例名与测试名不能包含下划线

1.1 C++ 保留标识符规则

googletest 官方 FAQ 给出的第一条理由与 C++ 语言标准直接相关:编译器与标准库保留了以下两类标识符:

  1. 以_开头、后接大写字母的标识符(如_Foo);
  2. 名称中任意位置出现连续两个下划线(__)的标识符。

用户代码被禁止使用这两类标识符。问题在于TEST(TestCaseName, TestName)宏会展开并生成一个名为TestCaseName_TestName_Test的类:

  • 若TestCaseName以_开头且后接大写字母(如_Foo),生成的_Foo_TestName_Test属于保留标识符,非法;
  • 若TestCaseName以_结尾(如Foo_),生成的Foo__TestName_Test含连续双下划线,非法;
  • 若TestName以_开头或结尾,同样会在名称中制造出__,非法。

因此,TestCaseName与TestName都不能以_开头或结尾(严格说TestCaseName以_开头只要后接小写字母即可,但官方为简单起见统一禁止)。

1.2 中间出现下划线也会引发类名冲突

即便下划线出现在名称中间,也会带来命名冲突问题。考虑官方文档给出的例子:

TEST(Time, Flies_Like_An_Arrow) { ... } TEST(Time_Flies, Like_An_Arrow) { ... }

两个测试宏分别展开后,生成的类名都是Time_Flies_Like_An_Arrow_Test,发生冲突。所以官方干脆一刀切:在TestCaseName和TestName中完全避免下划线。这条规则比必要限制更严格,但简单、易记,也为 googletest 未来实现演进留出余地。

提示:违反该规则短期内可能看不出问题,但换用新编译器或 googletest 新版本后,测试可能莫名编译失败——这是官方明确提示的“潜在风险”,并非危言耸听。

二、断言宏与 NULL:为什么EXPECT_EQ(NULL, ptr)可行而EXPECT_NE(NULL, ptr)不可行

2.1 首选nullptr,而非NULL

FAQ 明确指出:EXPECT_NE(nullptr, ptr)与ASSERT_NE(nullptr, ptr)完全可用,且是代码风格指南中推荐写法,因为nullptr没有NULL的模板类型推导问题——这正是NULL无法直接用于EXPECT_NE的根源。

2.2 模板元编程的成本权衡

EXPECT_XX()/ASSERT_XX()系列宏要支持NULL作为参数,需要引入非平凡的模板元编程技巧。官方只在需求最强烈的地方实现:

  • EXPECT_EQ(a, b)语义上第一个参数是期望值、第二个是实际值,EXPECT_EQ(NULL, some_expression)的需求被多次提出,因此实现了;
  • EXPECT_NE(NULL, ptr)的需求没那么强——断言失败时你本来就已知道ptr必为NULL,此时打印ptr不增加任何信息,写EXPECT_TRUE(ptr != NULL)效果相同;
  • 若支持EXPECT_NE(NULL, ptr),为保持一致性还须支持EXPECT_NE(ptr, NULL)(EXPECT_NE没有参数顺序约定),等于模板技巧要用两次,维护成本翻倍,收益不值得。

2.3 更统一的出路:matcher 语法

随着 gMock matcher 库的成长,官方正鼓励测试统一改用EXPECT_THAT(value, matcher)语法。matcher 的显著优势是可组合性:matcher 之间可以自由组合成新 matcher,而EXPECT_NE这类宏难以组合。TEN-framework 的 smoke 测试大量使用ASSERT_EQ(rc, true)、check_status_code、check_detail_with_string这类封装好的断言(见 basic_hello_world_1.cc),其本质正是基于 googletest 断言宏的二次封装,适合在项目中沉淀公共断言工具。

三、测试同一接口的多个实现:typed tests 还是 value-parameterized tests?

两者都能完成“验证接口的所有实现满足共同需求”的任务,选哪个取决于你的具体场景。FAQ 给出了五条实用判断准则:

考量维度Typed TestsValue-parameterized Tests
实例创建方式各实现可用同一种方式创建(如都有公共默认构造函数,可写new TypeParam,或工厂函数形式一致如CreateInstance<TypeParam>())时更易写各实现需要不同代码模式创建(如new Foovsnew Bar(5))时更易写,可写工厂函数包装器把函数指针作为参数传入
失败定位失败输出包含类型名,能快速定位是哪个实现出问题只能靠迭代序号推断是哪个实现,不够直观
编译错误模板化代码导致编译错误更难消化编译错误相对友好
接口约束必须确保测试针对接口类型而非具体类型,即保证implicit_cast<MyInterface*>(my_concrete_impl)成立这方面不易犯错

FAQ 的最终建议很朴素:两种都试试,实践才是掌握二者细微差别的最好方式。需要留意的是,typed tests 一旦写错,模板化代码的编译错误信息会比较难读;而 value-parameterized tests 的失败定位需要借助迭代序号。

四、死亡测试(Death Test)专题

死亡测试用于验证“某段代码会按预期崩溃/终止”,是 googletest 中最复杂也最易踩坑的机制。FAQ 用了多个条目专门讲解。

4.1 为什么死亡测试变慢了

2008 年 8 月起,由于线程化日志成为默认配置,默认死亡测试风格从fast切换为threadsafe,导致大量死亡测试变慢。这是必要代价。官方指引参考 Fixing Failing Death Tests(仓库内对应死亡测试章节)。

4.2 死亡测试修改的状态为何“丢失”

EXPECT_DEATH等死亡断言在子进程中执行,以保证预期崩溃不会杀死测试程序(父进程)。因此,子进程产生的任何内存副作用只在各自子进程内可见,父进程完全观察不到——可以理解为它们运行在一个“平行宇宙”。FAQ 特别提醒:若使用 gMock(仓库内位于 third_party/googlemock),死亡测试语句中调用的 mock 方法,父进程会认为从未发生过,因此应把EXPECT_CALL语句移进EXPECT_DEATH宏内部。

4.3 死亡测试挂起或段错误

googletest 的死亡测试在子进程中运行,机制相当微妙。FAQ 给出修复路线:

  • 首要排查:消除EXPECT_DEATH()之外创建的线程。死亡测试不喜欢父进程存在多线程,可用 mock 或 fake 对象替代真实对象;
  • 若所用库在进入main()之前就创建了线程,则把尽可能多的活动移进EXPECT_DEATH()(极端情况全部移入),或反过来尽可能少留;也可尝试将死亡测试风格设为"threadsafe"(更安全但更慢);
  • 线程安全的死亡测试会在子进程中从头重跑测试程序,因此必须保证程序可并排运行自身、且行为确定;
  • 归根结底是并发编程问题:确保程序无竞态、无死锁,没有银弹。

4.4 已 join 的线程仍触发 ASSERT_DEATH 报错

在旧 Linux pthread 库下,一旦从单线程跨入多线程就“回不去”:首次创建线程时额外产生一个 manager 线程(得到 3 个而非 2 个线程);之后线程 join 回主线程,计数减 1,但 manager 线程永不消亡,仍剩 2 个线程,因此无法安全运行死亡测试。新的 NPTL 线程库没有此问题(不创建 manager 线程),但若无法控制测试运行机器,就不应依赖这一点。

4.5 为什么包含 ASSERT_DEATH 的整个测试用例必须命名为*DeathTest

googletest 不会交错执行不同测试用例的测试:它先跑完一个用例的所有测试,再跑下一个,因为需要在用例首测试前 set-up、结束后 tear-down。若按测试名而非用例名决定执行顺序,就会产生矛盾场景:

TEST_F(FooTest, AbcDeathTest) { ... } TEST_F(FooTest, Uvw) { ... } TEST_F(BarTest, DefDeathTest) { ... } TEST_F(BarTest, Xyz) { ... }

FooTest.AbcDeathTest须先于BarTest.Xyz,同时BarTest.DefDeathTest又须先于FooTest.Uvw,而不同用例不允许交错执行,两者矛盾。这是 googletest 要求整个用例命名为*DeathTest的根本原因。

4.6 不想把整个用例命名为 DeathTest 怎么办

可以拆分用例,名称清晰表达相关性:

class FooTest : public ::testing::Test { ... }; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } using FooDeathTest = FooTest; TEST_F(FooDeathTest, Uvw) { ... EXPECT_DEATH(...) ... } TEST_F(FooDeathTest, Xyz) { ... ASSERT_DEATH(...) ... }

4.7 ASSERT_DEATH 的 statement 参数可以是什么

ASSERT_DEATH(statement, regex)(及任何死亡断言宏)的statement只要在当前上下文合法即可,可以是:

  • 简单函数调用;
  • 引用全局/局部变量的复杂表达式;
  • 复合语句(compound statement)。

FAQ 给出的完整示例:

// 简单函数调用 TEST(MyDeathTest, FunctionCall) { ASSERT_DEATH(Xyz(5), "Xyz failed"); } // 引用变量和函数的复杂表达式 TEST(MyDeathTest, ComplexExpression) { const bool c = Condition(); ASSERT_DEATH((c ? Func1(0) : object2.Method("test")), "(Func1|Method) failed"); } // 循环内的死亡断言 TEST(MyDeathTest, InsideLoop) { for (int i = 0; i < 5; i++) { EXPECT_DEATH_M(Foo(i), "Foo has \\d+ errors", ::testing::Message() << "where i is " << i); } } // 复合语句 TEST(MyDeathTest, CompoundStatement) { ASSERT_DEATH({ for (int i = 0; i < 5; i++) { Bar(i); } }, "Bar has \\d+ errors"); }

仓库内的 gtest 自带测试 gtest-death-test_test.cc 包含更多相关示例可供研读。

4.8 成功时看不到子进程 LOG 消息

打印EXPECT_DEATH()内语句产生的 LOG 会干扰父进程日志中真实问题的检索,因此 googletest 只在死亡测试失败时打印这些 LOG。临时变通办法:故意破坏死亡测试(例如改掉期望匹配的正则)使其失败以观察日志——FAQ 承认这是 hack,并计划在 fork-and-exec 风格死亡测试落地后给出更永久的方案。

五、协议缓冲区(Protobuf)断言:ProtocolMessageEquals已弃用

ProtocolMessageEquals与ProtocolMessageEquiv现已弃用,应改用EqualsProto等。弃用的旧 matcher 对无效的 protocol buffer 定义更不宽容:如果foo.proto未完整限定所引用消息的类型(如写了message<Bar>而应为message<blah.Bar>),运行时会出现类似错误:

... descriptor.cc:...] Invalid proto descriptor for file "path/to/foo.proto": ... descriptor.cc:...] blah.MyMessage.my_field: ".Bar" is not defined.

看到这类报错说明.proto文件本身有缺陷,需要把类型写成完全限定名。新定义只是恰好暴露了你的 bug。

六、常见编译错误的成因与修复

6.1EXPECT_EQ(htonl(blah), blah_blah)在 opt 模式下的诡异编译错误

这不是 googletest 的 bug,而是htonl()的问题:按man htonl,它是函数,可作为函数指针使用;但在 opt 模式下htonl()被定义为宏,且该宏使用 gcc 扩展、并非标准 C++,存在临时性局限——它阻止你写Foo<sizeof(htonl(x))>()这类带整型参数的模板。而EXPECT_EQ(a, b)的实现恰好在模板参数内使用sizeof(... a ...),因此 opt 模式下a含htonl()调用时无法编译。由于解决方案必须在各平台不同编译器下都成立,难以让EXPECT_EQ绕过此缺陷。官方建议改用ghtonl()(htons()对应ghtons()),并记得在使用处把//util/endian加入 BUILD 依赖——它只含一个头文件,不会撑大二进制。

6.2 类体内定义的静态 const 成员出现 “undefined reference”

在类体内这样写只是声明:

// foo.h class Foo { ... static const int kBar = 100; };

还必须在foo.cc中于类体外定义:

const int Foo::kBar; // 无需初始化器

否则代码属于非法 C++,可能以意外方式损坏。尤其是用于 googletest 比较断言(EXPECT_EQ等)时会产生 “undefined reference” 链接错误。FAQ 提醒:“以前能跑”不代表合法,只是运气好。

6.3 “void value not ignored as it ought to be”

这通常意味着你在不返回 void 的函数里使用了ASSERT_*()。由于构建系统禁用了异常,ASSERT_*()只能在 void 函数中使用,详见 advanced.md 的 Assertion Placement 章节。

6.4 构造函数(或析构函数)不能返回值

为了支持向断言流式写入消息的语法:

ASSERT_EQ(1, Foo()) << "blah blah" << foo;

googletest 不得不放弃在构造函数和析构函数中使用ASSERT*和FAIL*(EXPECT*和ADD_FAILURE*不受影响)。变通方案:把构造函数/析构函数内容移到私有 void 成员函数中,或改用EXPECT_*()。参见 advanced.md 的 Assertion Placement 章节。

6.5 “no match for 'operator<<'”

若在断言中使用自定义类型FooType,必须定义std::ostream& operator<<(std::ostream&, const FooType&)以便打印其值。若FooType声明在命名空间中,<<运算符也必须定义在同一命名空间。

6.6ASSERT_PRED*的 “no matching function to call”

谓词函数若为重载函数或模板,编译器无法确定选哪个版本。ASSERT_PRED_FORMAT*/EXPECT_PRED_FORMAT*没有此问题。修复方式有二:

  1. 改用(ASSERT|EXPECT)_PRED_FORMAT*(附带更好的失败消息);
  2. 显式指定版本,例如:
EXPECT_PRED1(static_cast<bool (*)(int)>(IsPositive), 5); // 明确选择 int 版本

模板谓词可显式实例化:

ASSERT_PRED1(IsNegative<int>, -5);

多参数模板函数需注意宏参数计数问题——ASSERT_PRED2(GreaterThan<int, int>, 5, 0)会被预处理器认为传了 4 个参数,必须用括号包住谓词:

ASSERT_PRED2((GreaterThan<int, int>), 5, 0);

6.7 忽略RUN_ALL_TESTS()返回值

把return RUN_ALL_TESTS();写成RUN_ALL_TESTS();是错误且危险的:测试服务需要该返回值判断测试是否通过,main()忽略它会导致即便断言失败测试也被视为成功。googletest 已在 gcc 下禁止忽略其返回值,违反即编译报错,修复就是确保它作为main()的返回值。

TEN-framework 的 smoke 测试入口正是这样做的——gtest_main.cc 中:

GTEST_API_ int main(int argc, char **argv) { printf("Running main() from %s\n", __FILE__); testing::InitGoogleTest(&argc, argv); // Add the environment to Google Test. ::testing::AddGlobalTestEnvironment(new GlobalTestEnvironment); return RUN_ALL_TESTS(); }

可以看到仓库还在RUN_ALL_TESTS()之前通过AddGlobalTestEnvironment注册了一个全局环境(GlobalTestEnvironment),在SetUp()中创建 fake app 线程并等待其完成配置、在TearDown()中关闭并回收 app 线程——这是把 googletest 全局环境机制用于集成测试场景的典型范例。

6.8TEST_F(FooTest, Bar)报 “no matching function for call to FooTest::FooTest()”

googletest 需要能创建 fixture 对象,因此 fixture 类必须存在默认构造函数(通常编译器会自动生成)。需要手写的情况:

  • 显式声明了非默认构造函数(如DISALLOW_EVIL_CONSTRUCTORS()的副作用)时,须补一个默认构造函数,哪怕是空的;
  • fixture 含 const 非静态数据成员时,必须在默认构造函数的初始化列表中初始化该 const 成员(早期 gcc 不强制,属于已在 gcc 4 修复的 bug)。

七、Test Fixture 的正确用法

7.1 能否从一个 fixture 派生另一个?——可以

每个 test fixture 有同名且唯一的 test case。多个 test case 想共享同一(或相近的)fixture 时,把共享逻辑放进基类 fixture,再为每个 test case 派生专用 fixture,用TEST_F()编写测试。典型结构:

// 定义基类 fixture class BaseTest : public ::testing::Test { protected: ... }; // 从 BaseTest 派生 FooTest class FooTest : public BaseTest { protected: void SetUp() override { BaseTest::SetUp(); // 先设置基类 fixture ... additional set-up work ... } void TearDown() override { ... clean-up work for FooTest ... BaseTest::TearDown(); // 记得在清理完 FooTest 后再拆卸基类 } ... functions and variables for FooTest ... }; // 使用 FooTest 的测试 TEST_F(FooTest, Bar) { ... } TEST_F(FooTest, Baz) { ... }

必要时还可以从派生 fixture 继续派生,googletest 对继承深度没有限制。完整示例见仓库内的 sample5_unittest.cc。

7.2 构造函数/析构函数 还是 SetUp()/TearDown()?

关键前提:googletest不会跨测试复用同一个 fixture 对象。对每个TEST_F,它都会创建全新的fixture 对象、立即调用SetUp()、运行测试体、调用TearDown(),然后销毁对象。

构造函数/析构函数通常更受青睐,因为:

  • 在构造函数中初始化成员变量时,可将其声明为const,防止意外改动、使测试更显然正确;
  • 子类化 fixture 时,子类构造函数保证先调用基类构造函数、子类析构函数保证后调用基类析构函数;而SetUp()/TearDown()下子类可能忘记调用基类版本或在错误时机调用。

少数情况下仍需SetUp()/TearDown():

  1. 构造函数体内无法使用ASSERT_xx宏,若 set-up 操作可能产生致命失败(应阻止测试继续),须用CHECK宏或改用SetUp();
  2. tear-down 可能抛异常时必须用TearDown()——析构函数中抛异常是未定义行为,通常直接杀死程序;注意 STL 等标准库在启用异常时可能抛出,想写可移植测试应优先TearDown();
  3. googletest 团队正考虑在启用异常的平台(Windows、Mac OS、Linux 客户端)让断言宏直接抛出,届时不再需要用户向调用方传播失败——因此若代码可能运行于这类平台,不要在析构函数中使用断言;
  4. 构造函数/析构函数中对本对象无法做虚函数调用(声明为 virtual 的方法也会被静态绑定),若需调用将被派生类覆写的方法,必须用SetUp()/TearDown()。

7.3SetUp()为什么没被调用

C++ 区分大小写。你是不是写成了Setup()?同理,把SetUpTestCase()写成SetupTestCase()也会静默失效。

7.4 多个 test case 共享 fixture 逻辑,必须逐个定义新 fixture 类吗?——不必

与其写:

class FooTest : public BaseTest {}; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } class BarTest : public BaseTest {}; TEST_F(BarTest, Abc) { ... } TEST_F(BarTest, Def) { ... }

不如直接typedef:

typedef BaseTest FooTest; TEST_F(FooTest, Abc) { ... } TEST_F(FooTest, Def) { ... } typedef BaseTest BarTest; TEST_F(BarTest, Abc) { ... } TEST_F(BarTest, Def) { ... }

7.5 为什么优先用 fixture 而非全局变量

  • 测试很可能需要改变全局变量状态,副作用易从一个测试泄漏、污染其他测试;fixture 让每个测试拥有全新但同名的变量集合,测试彼此独立;
  • 全局变量污染全局命名空间;
  • fixture 可通过子类化复用(多个 test case 有共性的场景),全局变量难以做到。

7.6 同名的 TEST 方法可以出现在不同命名空间吗?

规则是:同一 test case 的所有测试方法必须使用同一个 fixture 类。因此下面这种写法允许(两者都用::testing::Test):

namespace foo { TEST(CoolTest, DoSomething) { SUCCEED(); } } // namespace foo namespace bar { TEST(CoolTest, DoSomething) { SUCCEED(); } } // namespace bar

而下面这种不允许,会触发 googletest 运行时错误(同一 test case 名使用了不同的 fixture 类):

namespace foo { class CoolTest : public ::testing::Test {}; TEST_F(CoolTest, DoSomething) { SUCCEED(); } } // namespace foo namespace bar { class CoolTest : public ::testing::Test {}; TEST_F(CoolTest, DoSomething) { SUCCEED(); } } // namespace bar

八、输出、调试与测试管理的实用技巧

8.1 在 Emacs 中直接跳到失败行

googletest 的失败消息格式被 Emacs 及 acme、XCode 等众多 IDE 识别。若消息出现在 Emacs 的编译缓冲区中,它可以直接点击跳转。

8.2 测试输出被 LOG 淹没

googletest 输出设计为简洁、人性化的报告;测试自身产生的文本输出会与之混杂。解决方案很简单:LOG 消息走 stderr,googletest 输出走 stdout,用重定向分离即可:

$ ./my_test > gtest_output.txt

8.3 代码如何检测自己是否运行在测试中?——不建议

在测试环境中嗅探并改变行为,等于把测试专用逻辑泄漏进生产代码,无法保证测试专用路径不会在生产中误运行,还会引发 Heisenbug(观测改变结果)类问题,googletest 不提供该能力。推荐做法是依赖注入:测试与生产代码注入不同功能,生产代码根本不链接测试逻辑。若实在万不得已,可遵循“测试程序名以_test结尾”的约定,在main()中嗅探argv[0]——FAQ 直言这是“可怕的 hack”。

8.4 如何临时禁用某个测试

给测试名加DISABLED_前缀即可排除其执行:

TEST(MyTest, DISABLED_ThatBrokenTest) { ... }

这优于注释掉代码或#if 0,因为被禁用的测试仍参与编译,不会“腐烂”。需要运行被禁用的测试时,用--gtest_also_run_disabled_tests标志调用测试程序。

8.5 如何抑制 Windows 上的内存泄漏报告

由于静态初始化的 googletest 单例需要在堆上分配,Visual C++ 的内存泄漏检测器会在程序结束时报告泄漏。规避方式是使用_CrtMemCheckpoint与_CrtMemDumpAllObjectsSince,不报告任何静态初始化的堆对象。

九、TEN-framework 中的 googletest 落地实践

结合仓库源码可以看到 googletest 在 TEN-framework 中的真实用法:

  • 依赖声明:smoke 测试的 BUILD.gn 通过public_deps引入//third_party/googlemock与//third_party/googletest,googletest 以仓库内 third_party 源码形式参与构建;
  • 测试组织:tests/ten_runtime/smoke 下按功能目录(basic、command、extension、engine、concurrent等)组织大量.cc测试文件,每个文件内部用匿名命名空间包裹测试辅助类、以TEST(BasicTest, HelloWorld1)形式组织用例;
  • 真实断言用法:basic_hello_world_1.cc 中,TEST内创建 app 线程、建立 msgpack TCP 客户端、发送 start_graph 命令与自定义hello_world命令,并用ten_test::check_status_code/check_detail_with_string校验结果——测试体与 fixture 机制的配合方式完全遵循本文所述规范(用例名BasicTest无下划线、fixture 默认构造等);
  • 自定义入口与全局环境:gtest_main.cc 展示了在main()中调用testing::InitGoogleTest、AddGlobalTestEnvironment注册全局环境(其SetUp/TearDown负责 fake app 生命周期)、最终return RUN_ALL_TESTS()的标准范式,与 FAQ 中关于RUN_ALL_TESTS()返回值的要求完全一致。

结语

googletest 的许多“怪癖”并非缺陷,而是 C++ 语言规则与框架设计取舍的必然结果。理解命名保留规则、NULL的模板推导局限、死亡测试的子进程模型、fixture 的生命周期与继承机制,以及各类编译错误的真实成因,能让你的测试代码在编译器升级、googletest 版本迭代时依然稳健。若想进一步系统学习,可继续阅读仓库内的 Primer.md 与 advanced.md,并结合 Samples.md 中的示例逐一实践。

  • 人工智能
  • AI Agent
  • 多模态
  • 语音
  • AI 应用

【免费下载链接】ten-framework

Open-source framework for conversational voice AI agents

项目地址:https://gitcode.com/TEN-framework/ten-framework
点击查看免费下载

相关推荐

上一篇:KubeSphere 镜像仓库访问的核心引擎:深入解析 go-containerregistry `remote` 包
下一篇:华硕笔记本硬件控制的现代化解决方案:G-Helper技术深度解析与实践指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 2:39:28

Django+ECharts构建网易云音乐可视化大屏:从数据清洗到用户画像实战

简介&#xff1a;一份面向高校计算机专业学生与科研从业者的网易云音乐可视化项目资料包&#xff0c;基于Python与Django框架构建数据大屏&#xff0c;聚焦用户画像与播放行为分析&#xff0c;适合用作毕业设计、课程设计或项目初期演示。资源共54个文件&#xff0c;以24个Pyth…

作者头像 李华
网站建设 2026/9/28 2:38:26

RV1106 ISP调试环境搭建:MATLAB仿真与在线调参工具联动

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:36:54

大麦网自动抢票脚本 3 步上手:新手快速启动教程

大麦网自动抢票脚本 3 步上手&#xff1a;新手快速启动教程 【免费下载链接】Automatic_ticket_purchase 大麦网抢票脚本 项目地址: https://gitcode.com/GitHub_Trending/au/Automatic_ticket_purchase 开抢前 3 秒&#xff0c;你的手指悬在"立即购买"上——…

作者头像 李华
网站建设 2026/9/28 2:36:54

C# WinForm酒店管理系统源码解析:从数据库设计到前台实战

简介&#xff1a;面向C#初学者的酒店管理系统项目源码&#xff0c;基于WinForm界面框架实现&#xff0c;覆盖用户管理、房客管理、客房管理和出入管理四大核心模块&#xff0c;适合用于课程设计、毕业设计或入门企业级桌面应用开发。资源压缩包共54个文件&#xff0c;整体仅159…

作者头像 李华