lenny.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8 -*-
  3. import audioop
  4. import contextlib
  5. import glob
  6. import io
  7. import os
  8. import string
  9. import subprocess
  10. import syslog
  11. import threading
  12. import urllib2
  13. import wave
  14. from datetime import datetime
  15. import linphone
  16. from enum import Enum
  17. VOLUME_THRESHOLD = 100
  18. def slugify(s):
  19. """
  20. Normalizes string, converts to lowercase, removes non-alpha characters,
  21. and converts spaces to hyphens, wich is url/filename friendly.
  22. """
  23. valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
  24. filename = ''.join(c for c in s if c in valid_chars)
  25. filename = filename.replace(' ', '_') # I don't like spaces in filenames.
  26. return filename
  27. class ConversationStatus(Enum):
  28. READY_TO_TALK = 0
  29. IMTALKING = 1
  30. WAITFORANSWER = 2
  31. class Conversation(object):
  32. def __init__(self):
  33. self._status = ConversationStatus.READY_TO_TALK
  34. @property
  35. def status(self):
  36. return self._status
  37. @status.setter
  38. def status(self, value):
  39. if value != self._status:
  40. self._status = value
  41. current_dir = os.path.dirname(os.path.realpath(__file__))
  42. replies_seq = glob.glob(current_dir + "/replies/sequence/*.wav")
  43. replies_seq.sort()
  44. replies_generic = glob.glob(current_dir + "/replies/generic/*.wav")
  45. replies_generic.sort()
  46. incoming_stream_file = "/tmp/lenny"
  47. conversation = Conversation()
  48. replies_pos = 0
  49. THREADS_MUST_QUIT = False
  50. KTIP_LOOKUP_URL = "https://www.ktipp.ch/service/warnlisten/detail/?warnliste_id=7&ajax=ajax-search-form&keyword={" \
  51. "$number$}"
  52. def is_in_local_blacklist(a_number):
  53. black_list = current_dir + "/blacklist.txt"
  54. if os.path.isfile(black_list):
  55. return a_number in open(current_dir + "/blacklist.txt").read()
  56. def is_in_ktipp_blacklist(a_number):
  57. # On peut interroger le site ktipp:
  58. # https://www.ktipp.ch/service/warnlisten/detail/?warnliste_id=7&ajax=ajax-search-form&keyword=0445510503
  59. # Si argument keyword pas trouvé, ca donne ca dans la réponse :
  60. # 0 Einträge
  61. url = KTIP_LOOKUP_URL.replace("{$number$}", a_number)
  62. return "0 Eintr" not in urllib2.urlopen(url).read()
  63. def log(msg):
  64. syslog.syslog(msg)
  65. print(msg)
  66. def get_wav_duration(fname):
  67. with contextlib.closing(wave.open(fname, 'r')) as f:
  68. frames = f.getnframes()
  69. rate = f.getframerate()
  70. return frames / float(rate)
  71. def sleep(duration):
  72. dummy_event = threading.Event()
  73. dummy_event.wait(timeout=duration)
  74. def say(core):
  75. global conversation
  76. global replies_pos
  77. if conversation.status is not ConversationStatus.IMTALKING:
  78. conversation.status = ConversationStatus.IMTALKING
  79. # On joue les repliques en sequence, puis quand
  80. # on arrive au bout, on en joue une au hasard
  81. # du groupe 'generic'
  82. voice_filename = replies_seq[replies_pos]
  83. replies_pos = (replies_pos + 1) % len(replies_seq)
  84. if replies_pos == 0:
  85. # On ne rejoue jamais la première réplique "allo"
  86. replies_pos = 1
  87. duration = get_wav_duration(voice_filename)
  88. log("Saying : " + voice_filename + "(duration : " + str(duration) + ")")
  89. core.play_file = voice_filename
  90. sleep(duration)
  91. core.play_file = ""
  92. # On laisse l'autre l'occassion de reparler
  93. conversation.status = ConversationStatus.WAITFORANSWER
  94. def call_state_changed(core, call, state, message):
  95. global conversation
  96. global replies_pos
  97. log("state changed : " + message)
  98. if state == linphone.CallState.Released:
  99. # Let's convert wav to mp3
  100. log("Converting output from wav to mp3")
  101. if os.path.isfile(call.current_params.record_file):
  102. subprocess.call('lame --quiet --preset insane %s' % call.current_params.record_file, shell=True)
  103. os.remove(call.current_params.record_file)
  104. if state == linphone.CallState.IncomingReceived:
  105. log("Incoming call : {}".format(call.remote_address.username))
  106. replies_pos = 0
  107. if is_in_local_blacklist(call.remote_address.username) or is_in_ktipp_blacklist(call.remote_address.username):
  108. log("telemarketer calling : " + call.remote_address.username)
  109. call_params = core.create_call_params(call)
  110. a_file = current_dir + "/out/call_from_" + slugify(call.remote_address.username) + \
  111. "_" + datetime.now().strftime(
  112. '%Y-%m-%d_%Hh%Mmn%Ss') + ".wav"
  113. log(a_file)
  114. call_params.record_file = a_file
  115. # Let ring some time
  116. sleep(5)
  117. core.accept_call_with_params(call, call_params)
  118. call.start_recording()
  119. sleep(2)
  120. t = threading.Thread(target=incoming_stream_worker, args=[core, call])
  121. t.start()
  122. say(core)
  123. def global_state_changed(*args, **kwargs):
  124. log("global_state_changed: %r %r" % (args, kwargs))
  125. def registration_state_changed(core, call, state, message):
  126. log("registration_state_changed: " + str(state) + ", " + message)
  127. def incoming_stream_worker(core, call):
  128. global conversation
  129. log("Worker is starting")
  130. f = open(incoming_stream_file, "rb")
  131. f.seek(0, io.SEEK_END)
  132. p = f.tell()
  133. buf = ''
  134. previous_status = conversation.status
  135. while call.state is not linphone.CallState.End and not THREADS_MUST_QUIT:
  136. if conversation.status is ConversationStatus.IMTALKING:
  137. f.seek(0, io.SEEK_END)
  138. p = f.tell()
  139. else:
  140. if previous_status != conversation.status:
  141. f.seek(0, io.SEEK_END)
  142. p = f.tell()
  143. f.seek(p)
  144. buf += f.read(4096)
  145. p = f.tell()
  146. if len(buf) >= 20000:
  147. volume = audioop.rms(buf, 2)
  148. print("Detected volume : " + str(volume))
  149. # print("State : " + str(conversation.status))
  150. buf = ''
  151. if volume < VOLUME_THRESHOLD:
  152. if conversation.status is ConversationStatus.READY_TO_TALK:
  153. threading.Thread(target=say, args=[core]).start()
  154. else:
  155. conversation.status = ConversationStatus.READY_TO_TALK
  156. # We must sleep a bit to avoid cpu hog
  157. sleep(0.01)
  158. previous_status = conversation.status
  159. log("Worker is quitting")
  160. def main():
  161. log("lenny is starting ...")
  162. callbacks = {
  163. 'call_state_changed': call_state_changed,
  164. 'registration_state_changed': registration_state_changed,
  165. 'global_state_changed': global_state_changed,
  166. }
  167. username = "621"
  168. password = "toto"
  169. port = "5060"
  170. domain = "192.168.1.1"
  171. core = linphone.Core.new(callbacks, None, None)
  172. # On fait le setup pour la capture et analyse du stream entrant
  173. os.system("rm -rf " + incoming_stream_file)
  174. os.system("touch " + incoming_stream_file)
  175. core.use_files = True
  176. core.record_file = incoming_stream_file
  177. proxy_cfg = core.create_proxy_config()
  178. proxy_cfg.identity_address = core.create_address('sip:' + username + '@' + domain + ':' + port)
  179. proxy_cfg.server_addr = 'sip:' + domain + ':' + port
  180. proxy_cfg.register_enabled = True
  181. core.add_proxy_config(proxy_cfg)
  182. auth_info = core.create_auth_info(username, None, password, None, None, domain)
  183. core.add_auth_info(auth_info)
  184. while True:
  185. sleep(0.03)
  186. core.iterate()
  187. if __name__ == "__main__":
  188. try:
  189. main()
  190. except KeyboardInterrupt:
  191. THREADS_MUST_QUIT = True
  192. print "Bye"