feat: komplette Codebase vom Board ins Repo gespiegelt

app/ = /opt/callrec (Node-Backend, Vite-Frontend, Python-Inferenz,
Audio-Skripte, rkllm_server.cpp), system/ = systemd-Units,
WirePlumber/PipeWire-Drop-ins, sudoers. Modelle, Binaries, Aufnahmen
und node_modules bleiben draussen (.gitignore).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-07-16 11:18:29 +02:00
parent 560622e125
commit f9e1eb82c4
61 changed files with 5546 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
# CallRec USB-headset audio topology (persistent).
# Logitech H390 = user mic + user ears. UAC2 gadget = PC bridge.
# - PC audio (gadget capture, PC->device) -> headset sink (user hears Windows)
# - PC audio -> callmix (far side of the recording)
# - headset mic -> callmix (near side of the recording)
# - headset mic -> micgate -> gadget sink (device->PC) (far end / PC hears the user)
# CallRec records callmix.monitor (both sides).
# Idempotent: if callmix already exists, does nothing.
#
# NOISE GATE (2026-07-08): the user's boom mic picks up the far-end acoustically
# (proven: muting the Logi mic silences the gadget sink completely; NOT via the
# headset earcup). Teams/Discord AGC then renormalizes it, so lowering mic gain
# alone does nothing -- the far end hears itself with the loopback round-trip
# delay. Fix: route mic->PC THROUGH a LADSPA gate (aime-micgate.service, sink
# "micgate"). In the user's speech pauses the gate closes -> the gadget sink goes
# to absolute silence -> the far end can't hear itself; AGC can't amplify silence.
# Mic HW gain is kept LOW (not the old +11 dB max) on purpose: it keeps the idle
# bleed below the gate threshold (-45 dB), so the gate stays shut when the user is
# quiet even while the far end is loud.
set -u
export XDG_RUNTIME_DIR=/run/user/1000
PCAUD=alsa_input.platform-fc000000.usb.stereo-fallback # PC->device (Windows playing)
PCSINK=alsa_output.platform-fc000000.usb.stereo-fallback # device->PC (what Windows records)
HSSINK=alsa_output.usb-Logi_USB_Headset_Logi_USB_Headset-00.analog-stereo
HSMIC=alsa_input.usb-Logi_USB_Headset_Logi_USB_Headset-00.mono-fallback
# already configured?
if pactl list sinks short 2>/dev/null | grep -qw callmix; then
echo "callmix already present; nothing to do"
exit 0
fi
# wait for the gadget + headset PipeWire nodes to appear (headset/UAC2 enumerate
# a little after boot)
for i in $(seq 1 60); do
if pactl list sources short 2>/dev/null | grep -q "$PCAUD" \
&& pactl list sinks short 2>/dev/null | grep -q "$HSSINK" \
&& pactl list sources short 2>/dev/null | grep -q "$HSMIC"; then
break
fi
sleep 1
done
# wait briefly for the noise-gate sink (aime-micgate.service, a separate pipewire
# filter-chain client). Route the user's mic THROUGH it if present; fall back to
# the raw gadget sink so mic->PC still works even if the gate failed to start.
MIC_OUT="$PCSINK"
for i in $(seq 1 15); do
if pactl list sinks short 2>/dev/null | grep -qw micgate; then MIC_OUT=micgate; break; fi
sleep 1
done
# set the volume of a loopback's sink-input, found by its media.name tag (robust
# across reboots / stream-restore reshuffles, unlike numeric indices).
si_vol() { # $1 = media.name, $2 = volume (e.g. 60%)
local si
si=$(pactl list sink-inputs 2>/dev/null | awk -v n="$1" '
/^Sink Input #/{id=$3; gsub("#","",id)}
index($0, "media.name = \"" n "\""){print id; exit}')
[ -n "$si" ] && pactl set-sink-input-volume "$si" "$2" >/dev/null 2>&1
}
# headset mic hardware gain kept LOW (see NOISE GATE note): +1.4 dB, not +11 max.
# Low analog gain -> low idle bleed -> gate stays shut in the user's pauses.
amixer -c Headset sset Mic 12 unmute >/dev/null 2>&1 # belt (overridden by the source-vol below)
pactl set-source-volume "$HSMIC" 62% >/dev/null 2>&1 # WirePlumber-owned; 62%% -> HW Mic step 12 (~+1.4 dB). 100%% would map to +11 dB max and re-open the gate on far-end bleed.
# virtual recording mix
pactl load-module module-null-sink sink_name=callmix \
sink_properties=device.description=CallRecMix >/dev/null 2>&1
pactl set-sink-volume callmix 100% >/dev/null 2>&1
# routes — each loopback tagged so we can set per-stream gain by name
pactl load-module module-loopback source="$PCAUD" sink="$HSSINK" latency_msec=60 sink_input_properties=media.name=cr_pc2hs >/dev/null 2>&1 # PC -> headset (monitor)
pactl load-module module-loopback source="$PCAUD" sink=callmix latency_msec=60 sink_input_properties=media.name=cr_pc2mix >/dev/null 2>&1 # PC -> mix
pactl load-module module-loopback source="$HSMIC" sink=callmix latency_msec=60 sink_input_properties=media.name=cr_mic2mix >/dev/null 2>&1 # mic -> mix
pactl load-module module-loopback source="$HSMIC" sink="$MIC_OUT" latency_msec=60 sink_input_properties=media.name=cr_mic2pc >/dev/null 2>&1 # mic -> (gate ->) PC
# GAIN STAGING (clip-safe): PC media hits ~0 dBFS, so knock it down hard in the
# mix; the mic runs near unity. Their sum must never reach 0 dBFS or clipping
# corrupts whisper + diarization (over-segments to many speakers). PC monitoring
# to the headset (cr_pc2hs) stays at unity — full volume.
sleep 1
si_vol cr_pc2mix 60% # far side into the recording (~-12 dB)
si_vol cr_mic2mix 100% # near side (user), into the recording mix
si_vol cr_mic2pc 65% # near side to the far end (through the gate)
# NOTE: fine mic/PC balance still wants tuning on a REAL call. If the far end
# finds the user too quiet, raise cr_mic2pc / Mic gain -- but watch the gate:
# more gain -> more idle bleed -> may cross the -45 dB threshold and re-open the
# gate on the far-end bleed (echo returns). Retune the gate Threshold together.
# expose the mix as a REAL source (not a .monitor) so the CallRec UI lists it —
# recorder.js listSources() filters out every *.monitor.
pactl load-module module-remap-source master=callmix.monitor source_name=callrec_mix \
source_properties=device.description=CallRecMix >/dev/null 2>&1
# defaults: user hears via headset, CallRec records the mix
pactl set-default-sink "$HSSINK" >/dev/null 2>&1
pactl set-default-source callrec_mix >/dev/null 2>&1
echo "aime usb-audio topology loaded (mic->PC via micgate=$MIC_OUT)"