feat(voicemail): refactor VoicemailManager to use config for settings, add greeting conversion and removal methods
This commit is contained in:
@@ -14,8 +14,9 @@ from LXST.Sources import OpusFileSource
|
|||||||
|
|
||||||
|
|
||||||
class VoicemailManager:
|
class VoicemailManager:
|
||||||
def __init__(self, db, telephone_manager, storage_dir):
|
def __init__(self, db, config, telephone_manager, storage_dir):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
self.config = config
|
||||||
self.telephone_manager = telephone_manager
|
self.telephone_manager = telephone_manager
|
||||||
self.storage_dir = os.path.join(storage_dir, "voicemails")
|
self.storage_dir = os.path.join(storage_dir, "voicemails")
|
||||||
self.greetings_dir = os.path.join(self.storage_dir, "greetings")
|
self.greetings_dir = os.path.join(self.storage_dir, "greetings")
|
||||||
@@ -50,6 +51,10 @@ class VoicemailManager:
|
|||||||
else:
|
else:
|
||||||
RNS.log("Voicemail: ffmpeg not found", RNS.LOG_ERROR)
|
RNS.log("Voicemail: ffmpeg not found", RNS.LOG_ERROR)
|
||||||
|
|
||||||
|
def get_name_for_identity_hash(self, identity_hash):
|
||||||
|
"""Default implementation, should be patched by ReticulumMeshChat"""
|
||||||
|
return None
|
||||||
|
|
||||||
def _find_espeak(self):
|
def _find_espeak(self):
|
||||||
# Try standard name first
|
# Try standard name first
|
||||||
path = shutil.which("espeak-ng")
|
path = shutil.which("espeak-ng")
|
||||||
@@ -103,53 +108,82 @@ class VoicemailManager:
|
|||||||
# espeak-ng to WAV
|
# espeak-ng to WAV
|
||||||
subprocess.run([self.espeak_path, "-w", wav_path, text], check=True)
|
subprocess.run([self.espeak_path, "-w", wav_path, text], check=True)
|
||||||
|
|
||||||
# ffmpeg to Opus
|
# Convert WAV to Opus
|
||||||
if os.path.exists(opus_path):
|
return self.convert_to_greeting(wav_path)
|
||||||
os.remove(opus_path)
|
|
||||||
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
self.ffmpeg_path,
|
|
||||||
"-i",
|
|
||||||
wav_path,
|
|
||||||
"-c:a",
|
|
||||||
"libopus",
|
|
||||||
"-b:a",
|
|
||||||
"16k",
|
|
||||||
"-vbr",
|
|
||||||
"on",
|
|
||||||
opus_path,
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
return opus_path
|
|
||||||
finally:
|
finally:
|
||||||
if os.path.exists(wav_path):
|
if os.path.exists(wav_path):
|
||||||
os.remove(wav_path)
|
os.remove(wav_path)
|
||||||
|
|
||||||
|
def convert_to_greeting(self, input_path):
|
||||||
|
if not self.has_ffmpeg:
|
||||||
|
msg = "ffmpeg is required for audio conversion"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
opus_path = os.path.join(self.greetings_dir, "greeting.opus")
|
||||||
|
|
||||||
|
if os.path.exists(opus_path):
|
||||||
|
os.remove(opus_path)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
self.ffmpeg_path,
|
||||||
|
"-i",
|
||||||
|
input_path,
|
||||||
|
"-c:a",
|
||||||
|
"libopus",
|
||||||
|
"-b:a",
|
||||||
|
"16k",
|
||||||
|
"-vbr",
|
||||||
|
"on",
|
||||||
|
opus_path,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return opus_path
|
||||||
|
|
||||||
|
def remove_greeting(self):
|
||||||
|
opus_path = os.path.join(self.greetings_dir, "greeting.opus")
|
||||||
|
if os.path.exists(opus_path):
|
||||||
|
os.remove(opus_path)
|
||||||
|
return True
|
||||||
|
|
||||||
def handle_incoming_call(self, caller_identity):
|
def handle_incoming_call(self, caller_identity):
|
||||||
if not self.db.config.voicemail_enabled.get():
|
RNS.log(f"Voicemail: handle_incoming_call from {RNS.prettyhexrep(caller_identity.hash)}", RNS.LOG_DEBUG)
|
||||||
|
if not self.config.voicemail_enabled.get():
|
||||||
|
RNS.log("Voicemail: Voicemail is disabled", RNS.LOG_DEBUG)
|
||||||
return
|
return
|
||||||
|
|
||||||
delay = self.db.config.voicemail_auto_answer_delay_seconds.get()
|
delay = self.config.voicemail_auto_answer_delay_seconds.get()
|
||||||
|
RNS.log(f"Voicemail: Will auto-answer in {delay} seconds", RNS.LOG_DEBUG)
|
||||||
|
|
||||||
def voicemail_job():
|
def voicemail_job():
|
||||||
|
RNS.log(f"Voicemail: Auto-answer timer started for {RNS.prettyhexrep(caller_identity.hash)}", RNS.LOG_DEBUG)
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
# Check if still ringing and no other active call
|
# Check if still ringing and no other active call
|
||||||
telephone = self.telephone_manager.telephone
|
telephone = self.telephone_manager.telephone
|
||||||
|
if not telephone:
|
||||||
|
RNS.log("Voicemail: No telephone object", RNS.LOG_ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
RNS.log(f"Voicemail: Checking status. Call status: {telephone.call_status}, Active call: {telephone.active_call}", RNS.LOG_DEBUG)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
telephone
|
telephone
|
||||||
and telephone.active_call
|
and telephone.active_call
|
||||||
and telephone.active_call.get_remote_identity() == caller_identity
|
and telephone.active_call.get_remote_identity().hash == caller_identity.hash
|
||||||
and telephone.call_status == LXST.Signalling.STATUS_RINGING
|
and telephone.call_status == LXST.Signalling.STATUS_RINGING
|
||||||
):
|
):
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"Auto-answering call from {RNS.prettyhexrep(caller_identity.hash)} for voicemail",
|
f"Auto-answering call from {RNS.prettyhexrep(caller_identity.hash)} for voicemail",
|
||||||
RNS.LOG_DEBUG,
|
RNS.LOG_INFO,
|
||||||
)
|
)
|
||||||
self.start_voicemail_session(caller_identity)
|
self.start_voicemail_session(caller_identity)
|
||||||
|
else:
|
||||||
|
RNS.log("Voicemail: Auto-answer conditions not met after delay", RNS.LOG_DEBUG)
|
||||||
|
if telephone.active_call:
|
||||||
|
RNS.log(f"Voicemail: Active call remote: {RNS.prettyhexrep(telephone.active_call.get_remote_identity().hash)}", RNS.LOG_DEBUG)
|
||||||
|
|
||||||
threading.Thread(target=voicemail_job, daemon=True).start()
|
threading.Thread(target=voicemail_job, daemon=True).start()
|
||||||
|
|
||||||
@@ -170,7 +204,7 @@ class VoicemailManager:
|
|||||||
greeting_path = os.path.join(self.greetings_dir, "greeting.opus")
|
greeting_path = os.path.join(self.greetings_dir, "greeting.opus")
|
||||||
if not os.path.exists(greeting_path):
|
if not os.path.exists(greeting_path):
|
||||||
# Fallback if no greeting generated yet
|
# Fallback if no greeting generated yet
|
||||||
self.generate_greeting(self.db.config.voicemail_greeting.get())
|
self.generate_greeting(self.config.voicemail_greeting.get())
|
||||||
|
|
||||||
def session_job():
|
def session_job():
|
||||||
try:
|
try:
|
||||||
@@ -206,7 +240,7 @@ class VoicemailManager:
|
|||||||
self.start_recording(caller_identity)
|
self.start_recording(caller_identity)
|
||||||
|
|
||||||
# 4. Wait for max recording time or hangup
|
# 4. Wait for max recording time or hangup
|
||||||
max_time = self.db.config.voicemail_max_recording_seconds.get()
|
max_time = self.config.voicemail_max_recording_seconds.get()
|
||||||
start_wait = time.time()
|
start_wait = time.time()
|
||||||
while self.is_recording and (time.time() - start_wait < max_time):
|
while self.is_recording and (time.time() - start_wait < max_time):
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
@@ -271,7 +305,7 @@ class VoicemailManager:
|
|||||||
|
|
||||||
# Save to database if long enough
|
# Save to database if long enough
|
||||||
if duration >= 1:
|
if duration >= 1:
|
||||||
remote_name = self.telephone_manager.get_name_for_identity_hash(
|
remote_name = self.get_name_for_identity_hash(
|
||||||
self.recording_remote_identity.hash.hex()
|
self.recording_remote_identity.hash.hex()
|
||||||
)
|
)
|
||||||
self.db.voicemails.add_voicemail(
|
self.db.voicemails.add_voicemail(
|
||||||
|
|||||||
Reference in New Issue
Block a user