#!/usr/bin/env python3 """SIP Voice Notifier using Linphone CLI.""" import json import logging import os import subprocess import tempfile import time from urllib.parse import urlparse import requests from flask import Flask, request, jsonify from pydub import AudioSegment logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') _LOGGER = logging.getLogger(__name__) app = Flask(__name__) CONFIG = {} DEFAULT_SAMPLE_RATE = 8000 def setup_linphone_config(): """Create Linphone configuration.""" sip_user = CONFIG['sip_user'] sip_server = CONFIG['sip_server'] sip_password = CONFIG['sip_password'] config_dir = '/data/linphone' os.makedirs(config_dir, exist_ok=True) config_file = f'{config_dir}/linphonerc' config = f"""[sip] sip_port=5060 default_proxy=0 [proxy_0] reg_proxy=sip:{sip_server} reg_identity=sip:{sip_user}@{sip_server} reg_expires=3600 reg_sendregister=1 [auth_info_0] username={sip_user} userid={sip_user} passwd={sip_password} realm={sip_server} """ with open(config_file, 'w') as f: f.write(config) return config_file def download_and_convert_audio(url: str) -> str: """Download and convert audio.""" parsed = urlparse(url) ext = os.path.splitext(parsed.path)[1] or ".mp3" 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(8192): f.write(chunk) try: audio = AudioSegment.from_file(download_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(download_path) return wav_path except Exception as e: if os.path.exists(download_path): os.remove(download_path) raise Exception(f"Audio conversion failed: {e}") def generate_tts_audio(message: str) -> str: """Generate TTS audio.""" 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'] if not destination.startswith('sip:'): dest_uri = f"sip:{destination}@{sip_server}" else: dest_uri = destination _LOGGER.info(f"Calling {dest_uri} using Linphone...") try: cmd = ['linphonec', '-c', config_file, '-C'] process = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) # Wait for registration _LOGGER.info("Waiting for SIP registration...") time.sleep(5) # Make call _LOGGER.info(f"Calling {dest_uri}...") process.stdin.write(f"call {dest_uri}\n") process.stdin.flush() # Wait for answer _LOGGER.info("Waiting for answer...") time.sleep(5) # Play audio _LOGGER.info(f"Playing {audio_file}...") process.stdin.write(f"play {audio_file}\n") process.stdin.flush() # Keep call active _LOGGER.info(f"Call active for {duration}s...") time.sleep(duration) # Hangup _LOGGER.info("Ending call...") process.stdin.write("terminate\n") process.stdin.flush() time.sleep(2) process.stdin.write("quit\n") process.stdin.flush() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() _LOGGER.info("Call completed") except Exception as e: _LOGGER.error(f"Call error: {e}", exc_info=True) raise @app.route('/health', methods=['GET']) def health(): return jsonify({'status': 'ok'}), 200 @app.route('/send_notification', methods=['POST']) def handle_send_notification(): try: data = request.json destination = data.get('destination') audio_url = data.get('audio_url') message = data.get('message') duration = data.get('duration', CONFIG.get('default_duration', 30)) if not destination: return jsonify({'error': 'destination required'}), 400 if not audio_url and not message: return jsonify({'error': 'audio_url or message required'}), 400 temp_files = [] try: if audio_url: _LOGGER.info(f"Downloading: {audio_url}") audio_file = download_and_convert_audio(audio_url) temp_files.append(audio_file) else: _LOGGER.info(f"Generating TTS: {message}") audio_file = generate_tts_audio(message) temp_files.append(audio_file) place_sip_call(destination, audio_file, duration) return jsonify({'status': 'success'}), 200 finally: for f in temp_files: try: if os.path.exists(f): os.remove(f) except Exception as e: _LOGGER.warning(f"Cleanup failed: {e}") except Exception as e: _LOGGER.error(f"Request error: {e}", exc_info=True) return jsonify({'error': str(e)}), 500 if __name__ == '__main__': options_file = '/data/options.json' if os.path.exists(options_file): with open(options_file, 'r') as f: CONFIG = json.load(f) else: CONFIG = { 'sip_server': os.getenv('SIP_SERVER', ''), 'sip_user': os.getenv('SIP_USER', ''), 'sip_password': os.getenv('SIP_PASSWORD', ''), 'default_duration': int(os.getenv('DEFAULT_DURATION', 30)) } _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("=" * 60) app.run(host='0.0.0.0', port=8099, debug=False)