Switch to Debian base image with linphone-cli (v3.1.0)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""SIP Voice Notifier Add-on Service using Linphone CLI."""
|
||||
"""SIP Voice Notifier using Linphone CLI."""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -12,22 +12,16 @@ import requests
|
||||
from flask import Flask, request, jsonify
|
||||
from pydub import AudioSegment
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Global config
|
||||
CONFIG = {}
|
||||
DEFAULT_SAMPLE_RATE = 8000
|
||||
|
||||
|
||||
def setup_linphone_config():
|
||||
"""Create Linphone configuration file."""
|
||||
"""Create Linphone configuration."""
|
||||
sip_user = CONFIG['sip_user']
|
||||
sip_server = CONFIG['sip_server']
|
||||
sip_password = CONFIG['sip_password']
|
||||
@@ -37,28 +31,15 @@ def setup_linphone_config():
|
||||
|
||||
config_file = f'{config_dir}/linphonerc'
|
||||
|
||||
# Create Linphone configuration
|
||||
config_content = f"""[sip]
|
||||
config = f"""[sip]
|
||||
sip_port=5060
|
||||
sip_tcp_port=-1
|
||||
sip_tls_port=-1
|
||||
default_proxy=0
|
||||
|
||||
[rtp]
|
||||
audio_rtp_port=7078
|
||||
audio_adaptive_jitt_comp_enabled=1
|
||||
|
||||
[sound]
|
||||
playback_dev_id=ALSA: default device
|
||||
capture_dev_id=ALSA: default device
|
||||
|
||||
[proxy_0]
|
||||
reg_proxy=sip:{sip_server}
|
||||
reg_identity=sip:{sip_user}@{sip_server}
|
||||
reg_expires=3600
|
||||
reg_sendregister=1
|
||||
publish=0
|
||||
dial_escape_plus=0
|
||||
|
||||
[auth_info_0]
|
||||
username={sip_user}
|
||||
@@ -68,44 +49,36 @@ realm={sip_server}
|
||||
"""
|
||||
|
||||
with open(config_file, 'w') as f:
|
||||
f.write(config_content)
|
||||
f.write(config)
|
||||
|
||||
_LOGGER.info(f"Linphone config created: {config_file}")
|
||||
return config_file
|
||||
|
||||
|
||||
def download_and_convert_audio(url: str) -> str:
|
||||
"""Download and convert audio to WAV."""
|
||||
parsed_url = urlparse(url)
|
||||
extension = os.path.splitext(parsed_url.path)[1] or ".mp3"
|
||||
"""Download and convert audio."""
|
||||
parsed = urlparse(url)
|
||||
ext = os.path.splitext(parsed.path)[1] or ".mp3"
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=extension) as tmp:
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
|
||||
download_path = tmp.name
|
||||
|
||||
response = requests.get(url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
with open(download_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
for chunk in response.iter_content(8192):
|
||||
f.write(chunk)
|
||||
|
||||
_LOGGER.debug(f"Downloaded: {download_path}")
|
||||
|
||||
try:
|
||||
audio = AudioSegment.from_file(download_path)
|
||||
audio = audio.set_frame_rate(DEFAULT_SAMPLE_RATE)
|
||||
audio = audio.set_channels(1)
|
||||
audio = audio.set_sample_width(2)
|
||||
audio = audio.set_frame_rate(DEFAULT_SAMPLE_RATE).set_channels(1).set_sample_width(2)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
||||
wav_path = tmp.name
|
||||
|
||||
audio.export(wav_path, format="wav")
|
||||
_LOGGER.debug(f"Converted: {wav_path}")
|
||||
|
||||
os.remove(download_path)
|
||||
return wav_path
|
||||
|
||||
except Exception as e:
|
||||
if os.path.exists(download_path):
|
||||
os.remove(download_path)
|
||||
@@ -114,102 +87,79 @@ def download_and_convert_audio(url: str) -> str:
|
||||
|
||||
def generate_tts_audio(message: str) -> str:
|
||||
"""Generate TTS audio."""
|
||||
try:
|
||||
from gtts import gTTS
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
|
||||
mp3_path = tmp.name
|
||||
|
||||
tts = gTTS(text=message, lang='en')
|
||||
tts.save(mp3_path)
|
||||
|
||||
audio = AudioSegment.from_mp3(mp3_path)
|
||||
audio = audio.set_frame_rate(DEFAULT_SAMPLE_RATE)
|
||||
audio = audio.set_channels(1)
|
||||
audio = audio.set_sample_width(2)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
||||
wav_path = tmp.name
|
||||
|
||||
audio.export(wav_path, format="wav")
|
||||
os.remove(mp3_path)
|
||||
|
||||
return wav_path
|
||||
|
||||
except ImportError:
|
||||
raise Exception("gTTS not available")
|
||||
|
||||
|
||||
def place_sip_call_linphone(destination: str, audio_file: str, duration: int):
|
||||
"""Place a SIP call using Linphone CLI."""
|
||||
config_file = setup_linphone_config()
|
||||
from gtts import gTTS
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
|
||||
mp3_path = tmp.name
|
||||
|
||||
tts = gTTS(text=message, lang='en')
|
||||
tts.save(mp3_path)
|
||||
|
||||
audio = AudioSegment.from_mp3(mp3_path)
|
||||
audio = audio.set_frame_rate(DEFAULT_SAMPLE_RATE).set_channels(1).set_sample_width(2)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
||||
wav_path = tmp.name
|
||||
|
||||
audio.export(wav_path, format="wav")
|
||||
os.remove(mp3_path)
|
||||
return wav_path
|
||||
|
||||
|
||||
def place_sip_call(destination: str, audio_file: str, duration: int):
|
||||
"""Place SIP call using Linphone CLI."""
|
||||
config_file = setup_linphone_config()
|
||||
sip_server = CONFIG['sip_server']
|
||||
|
||||
# Build destination URI
|
||||
if not destination.startswith('sip:'):
|
||||
dest_uri = f"sip:{destination}@{sip_server}"
|
||||
else:
|
||||
dest_uri = destination
|
||||
|
||||
_LOGGER.info(f"Calling {dest_uri} with Linphone...")
|
||||
_LOGGER.info(f"Calling {dest_uri} using Linphone...")
|
||||
|
||||
try:
|
||||
# Use linphonec (Linphone console) to make the call
|
||||
# linphonec command format:
|
||||
# linphonec -c config_file -a -C (console mode, auto-answer calls to us)
|
||||
cmd = ['linphonec', '-c', config_file, '-C']
|
||||
|
||||
cmd = [
|
||||
'linphonec',
|
||||
'-c', config_file,
|
||||
'-C' # Console mode
|
||||
]
|
||||
|
||||
_LOGGER.info(f"Starting Linphone: {' '.join(cmd)}")
|
||||
|
||||
# Start linphonec process
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1
|
||||
text=True
|
||||
)
|
||||
|
||||
# Wait for registration
|
||||
_LOGGER.info("Waiting for SIP registration...")
|
||||
time.sleep(5)
|
||||
|
||||
# Make the call
|
||||
_LOGGER.info(f"Sending call command to: {dest_uri}")
|
||||
# Make call
|
||||
_LOGGER.info(f"Calling {dest_uri}...")
|
||||
process.stdin.write(f"call {dest_uri}\n")
|
||||
process.stdin.flush()
|
||||
|
||||
# Wait for call to establish
|
||||
_LOGGER.info("Waiting for call to connect...")
|
||||
# Wait for answer
|
||||
_LOGGER.info("Waiting for answer...")
|
||||
time.sleep(5)
|
||||
|
||||
# Play audio
|
||||
_LOGGER.info(f"Playing audio file: {audio_file}")
|
||||
_LOGGER.info(f"Playing {audio_file}...")
|
||||
process.stdin.write(f"play {audio_file}\n")
|
||||
process.stdin.flush()
|
||||
|
||||
# Keep call active
|
||||
_LOGGER.info(f"Keeping call active for {duration}s...")
|
||||
_LOGGER.info(f"Call active for {duration}s...")
|
||||
time.sleep(duration)
|
||||
|
||||
# Terminate call
|
||||
_LOGGER.info("Terminating call...")
|
||||
# Hangup
|
||||
_LOGGER.info("Ending call...")
|
||||
process.stdin.write("terminate\n")
|
||||
process.stdin.flush()
|
||||
time.sleep(2)
|
||||
|
||||
# Quit linphone
|
||||
process.stdin.write("quit\n")
|
||||
process.stdin.flush()
|
||||
|
||||
# Wait for process to end
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
@@ -218,19 +168,17 @@ def place_sip_call_linphone(destination: str, audio_file: str, duration: int):
|
||||
_LOGGER.info("Call completed")
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Linphone call error: {e}", exc_info=True)
|
||||
_LOGGER.error(f"Call error: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""Health check endpoint."""
|
||||
return jsonify({'status': 'ok'}), 200
|
||||
|
||||
|
||||
@app.route('/send_notification', methods=['POST'])
|
||||
def handle_send_notification():
|
||||
"""Handle send_notification service call."""
|
||||
try:
|
||||
data = request.json
|
||||
destination = data.get('destination')
|
||||
@@ -247,23 +195,20 @@ def handle_send_notification():
|
||||
temp_files = []
|
||||
|
||||
try:
|
||||
# Prepare audio
|
||||
if audio_url:
|
||||
_LOGGER.info(f"Downloading: {audio_url}")
|
||||
audio_file = download_and_convert_audio(audio_url)
|
||||
temp_files.append(audio_file)
|
||||
elif message:
|
||||
else:
|
||||
_LOGGER.info(f"Generating TTS: {message}")
|
||||
audio_file = generate_tts_audio(message)
|
||||
temp_files.append(audio_file)
|
||||
|
||||
# Place call using Linphone
|
||||
place_sip_call_linphone(destination, audio_file, duration)
|
||||
place_sip_call(destination, audio_file, duration)
|
||||
|
||||
return jsonify({'status': 'success', 'message': 'Call completed'}), 200
|
||||
return jsonify({'status': 'success'}), 200
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
for f in temp_files:
|
||||
try:
|
||||
if os.path.exists(f):
|
||||
@@ -277,14 +222,11 @@ def handle_send_notification():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Load config
|
||||
options_file = '/data/options.json'
|
||||
if os.path.exists(options_file):
|
||||
with open(options_file, 'r') as f:
|
||||
CONFIG = json.load(f)
|
||||
_LOGGER.info("Config loaded from options.json")
|
||||
else:
|
||||
_LOGGER.warning("No options.json found")
|
||||
CONFIG = {
|
||||
'sip_server': os.getenv('SIP_SERVER', ''),
|
||||
'sip_user': os.getenv('SIP_USER', ''),
|
||||
@@ -293,13 +235,9 @@ if __name__ == '__main__':
|
||||
}
|
||||
|
||||
_LOGGER.info("=" * 60)
|
||||
_LOGGER.info("SIP Voice Notifier Add-on Ready (Using Linphone)")
|
||||
_LOGGER.info("=" * 60)
|
||||
_LOGGER.info("SIP Voice Notifier Ready (Linphone CLI on Debian)")
|
||||
_LOGGER.info(f"SIP Server: {CONFIG.get('sip_server')}")
|
||||
_LOGGER.info(f"SIP User: {CONFIG.get('sip_user')}")
|
||||
_LOGGER.info("")
|
||||
_LOGGER.info("Using Linphone CLI for SIP calls")
|
||||
_LOGGER.info("This should work better than pyVoIP!")
|
||||
_LOGGER.info("=" * 60)
|
||||
_LOGGER.info("Starting Flask service on port 8099")
|
||||
|
||||
app.run(host='0.0.0.0', port=8099, debug=False)
|
||||
|
||||
Reference in New Issue
Block a user