1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8
9let
10 cfg = config.services.wyoming.openwakeword;
11
12 inherit (lib)
13 concatMap
14 mkOption
15 mkEnableOption
16 mkIf
17 mkPackageOption
18 types
19 ;
20
21 inherit (builtins)
22 toString
23 ;
24
25 inherit (utils)
26 escapeSystemdExecArgs
27 ;
28in
29
30{
31 options.services.wyoming.openwakeword = with types; {
32 enable = mkEnableOption "Wyoming openWakeWord server";
33
34 package = mkPackageOption pkgs "wyoming-openwakeword" { };
35
36 uri = mkOption {
37 type = strMatching "^(tcp|unix)://.*$";
38 default = "tcp://0.0.0.0:10400";
39 example = "tcp://192.0.2.1:5000";
40 description = ''
41 URI to bind the wyoming server to.
42 '';
43 };
44
45 customModelsDirectories = mkOption {
46 type = listOf types.path;
47 default = [ ];
48 description = ''
49 Paths to directories with custom wake word models (*.tflite model files).
50 '';
51 };
52
53 preloadModels = mkOption {
54 type = listOf str;
55 default = [
56 "ok_nabu"
57 ];
58 example = [
59 # wyoming_openwakeword/models/*.tflite
60 "alexa"
61 "hey_jarvis"
62 "hey_mycroft"
63 "hey_rhasspy"
64 "ok_nabu"
65 ];
66 description = ''
67 List of wake word models to preload after startup.
68 '';
69 };
70
71 threshold = mkOption {
72 type = numbers.between 0.0 1.0;
73 default = 0.5;
74 description = ''
75 Activation threshold (0.0-1.0), where higher means fewer activations.
76
77 See trigger level for the relationship between activations and
78 wake word detections.
79 '';
80 apply = toString;
81 };
82
83 triggerLevel = mkOption {
84 type = ints.unsigned;
85 default = 1;
86 description = ''
87 Number of activations before a detection is registered.
88
89 A higher trigger level means fewer detections.
90 '';
91 apply = toString;
92 };
93
94 extraArgs = mkOption {
95 type = listOf str;
96 default = [ ];
97 description = ''
98 Extra arguments to pass to the server commandline.
99 '';
100 };
101 };
102
103 config = mkIf cfg.enable {
104 systemd.services."wyoming-openwakeword" = {
105 description = "Wyoming openWakeWord server";
106 wants = [
107 "network-online.target"
108 ];
109 after = [
110 "network-online.target"
111 ];
112 wantedBy = [
113 "multi-user.target"
114 ];
115 serviceConfig = {
116 DynamicUser = true;
117 User = "wyoming-openwakeword";
118 # https://github.com/home-assistant/addons/blob/master/openwakeword/rootfs/etc/s6-overlay/s6-rc.d/openwakeword/run
119 ExecStart = escapeSystemdExecArgs (
120 [
121 (lib.getExe cfg.package)
122 "--uri"
123 cfg.uri
124 "--threshold"
125 cfg.threshold
126 "--trigger-level"
127 cfg.triggerLevel
128 ]
129 ++ (concatMap (model: [
130 "--preload-model"
131 model
132 ]) cfg.preloadModels)
133 ++ (concatMap (dir: [
134 "--custom-model-dir"
135 (toString dir)
136 ]) cfg.customModelsDirectories)
137 ++ cfg.extraArgs
138 );
139 CapabilityBoundingSet = "";
140 DeviceAllow = "";
141 DevicePolicy = "closed";
142 LockPersonality = true;
143 MemoryDenyWriteExecute = true;
144 PrivateDevices = true;
145 PrivateUsers = true;
146 ProtectHome = true;
147 ProtectHostname = true;
148 ProtectKernelLogs = true;
149 ProtectKernelModules = true;
150 ProtectKernelTunables = true;
151 ProtectControlGroups = true;
152 ProtectProc = "invisible";
153 ProcSubset = "all"; # reads /proc/cpuinfo
154 RestrictAddressFamilies = [
155 "AF_INET"
156 "AF_INET6"
157 "AF_UNIX"
158 ];
159 RestrictNamespaces = true;
160 RestrictRealtime = true;
161 RuntimeDirectory = "wyoming-openwakeword";
162 SystemCallArchitectures = "native";
163 SystemCallFilter = [
164 "@system-service"
165 "~@privileged"
166 ];
167 UMask = "0077";
168 };
169 };
170 };
171}