Init with audio captcha

This commit is contained in:
Hsiaoming Yang
2014-11-25 18:05:00 +08:00
commit 78448f953b
13 changed files with 216 additions and 0 deletions

0
captcha/__init__.py Normal file
View File

216
captcha/audio.py Normal file
View File

@@ -0,0 +1,216 @@
# coding: utf-8
import os
import copy
import wave
import struct
import random
WAVE_SAMPLE_RATE = 8000 # HZ
WAVE_HEADER = bytearray(
b'RIFF\x00\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00'
b'@\x1f\x00\x00@\x1f\x00\x00\x01\x00\x08\x00data'
)
WAVE_HEADER_LENGTH = len(WAVE_HEADER) - 4
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
def _read_wave_file(filepath):
w = wave.open(filepath)
data = w.readframes(-1)
w.close()
return data
def change_speed(body, speed=1):
"""Change the voice speed of the wave body."""
if speed == 1:
return body
length = int(len(body) * speed)
rv = bytearray(length)
step = 0
for v in body:
i = int(step)
while i < int(step + speed) and i < length:
rv[i] = v
i += 1
step += speed
return rv
def patch_wave_header(body):
"""Patch header to the given wave body.
:param body: the wave content body, it should be bytearray.
"""
assert isinstance(body, bytearray)
length = len(body)
padded = length + length % 2
total = WAVE_HEADER_LENGTH + padded
header = copy.copy(WAVE_HEADER)
# fill the total length position
header[4:8] = bytearray(struct.pack('<I', total))
header += bytearray(struct.pack('<I', length))
data = header + body
# the total length is even
if length != padded:
data = data + bytearray([0])
return data
def create_noise(length, level=4):
"""Create white noise for background"""
noise = bytearray(length)
adjust = 128 - level / 2
i = 0
while i < length:
v = random.randint(0, 256)
noise[i] = v % level + adjust
i += 1
return noise
def create_silence(length):
"""Create a piece of silence."""
data = bytearray(length)
i = 0
while i < length:
data[i] = 128
i += 1
return data
def change_sound(body, level=1):
assert isinstance(body, bytearray)
if level == 1:
return body
body = copy.copy(body)
for i, v in enumerate(body):
if v > 128:
v = (v - 128) * level + 128
v = max(int(v), 128)
v = min(v, 256)
elif v < 128:
v = 128 - (128 - v) * level
v = min(int(v), 128)
v = max(v, 0)
body[i] = v
return body
def mix_wave(src, dst):
"""Mix two wave body into one."""
if len(src) > len(dst):
# output should be longer
dst, src = src, dst
for i, sv in enumerate(src):
dv = dst[i]
if sv < 128 and dv < 128:
dst[i] = sv * dv / 128
else:
dst[i] = 2 * (sv + dv) - sv * dv / 128 - 256
return dst
BEEP = bytearray(_read_wave_file(os.path.join(DATA_DIR, 'beep.wav')))
END_BEEP = change_speed(BEEP, 1.4)
SILENCE = create_silence(WAVE_SAMPLE_RATE / 5)
class AudioCaptcha(object):
def __init__(self, voicedir=None):
if voicedir is None:
voicedir = DATA_DIR
self._voicedir = voicedir
self._cache = {}
def load(self):
for n in os.listdir(self._voicedir):
if len(n) == 1 and os.path.isdir(os.path.join(self._voicedir, n)):
self._load_data(n)
def _load_data(self, name):
dirname = os.path.join(self._voicedir, name)
data = []
for f in os.listdir(dirname):
filepath = os.path.join(dirname, f)
if f.endswith('.wav') and os.path.isfile(filepath):
data.append(bytearray(_read_wave_file(filepath)))
self._cache[name] = data
def twist_pick(self, key):
voice = random.choice(self._cache[key])
# random change speed
speed = random.randrange(90, 120) / 100.0
voice = change_speed(voice, speed)
# random change sound
level = random.randrange(80, 120) / 100.0
voice = change_sound(voice, level)
return voice
def pick_for_background(self):
key = random.choice(self._cache.keys())
voice = random.choice(self._cache[key])
voice = copy.copy(voice)
voice.reverse()
speed = random.randrange(8, 16) / 10.0
voice = change_speed(voice, speed)
level = random.randrange(2, 6) / 10.0
voice = change_sound(voice, level)
return voice
def create_background_noise(self, length, chars):
noise = create_noise(length, 4)
pos = 0
while pos < length:
sound = self.pick_for_background()
end = pos + len(sound) + 1
noise[pos:end] = mix_wave(sound, noise[pos:end])
pos = end + random.randint(0, WAVE_SAMPLE_RATE / 10)
return noise
def create_wave_body(self, chars):
voices = []
inters = []
for key in chars:
voices.append(self.twist_pick(key))
v = random.randint(WAVE_SAMPLE_RATE, WAVE_SAMPLE_RATE * 3)
inters.append(v)
durations = map(lambda a: len(a), voices)
l = max(durations) * len(chars) + reduce(lambda a, b: a + b, inters)
bg = self.create_background_noise(l, chars)
body = BEEP + SILENCE + BEEP + SILENCE + BEEP
# begin
pos = inters[0]
for i, v in enumerate(voices):
end = pos + len(v) + 1
bg[pos:end] = mix_wave(v, bg[pos:end])
pos = end + inters[i]
body += bg + END_BEEP
return body
def generate(self, chars):
if not self._cache:
self.load()
body = self.create_wave_body(chars)
return patch_wave_header(body)

BIN
captcha/data/0/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/1/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/2/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/3/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/4/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/5/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/6/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/7/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/8/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/9/default.wav Normal file
View File

Binary file not shown.

BIN
captcha/data/beep.wav Normal file
View File

Binary file not shown.