├── composer ├── migrations │ ├── __init__.py │ ├── 0002_song_created_at.py │ └── 0001_initial.py ├── __init__.py ├── model_conf │ ├── chars.txt │ ├── model_weights_34.h5 │ └── model_arc.json ├── utils.py ├── urls.py ├── admin.py ├── fetcher.py ├── writer.py ├── settings.py ├── templates │ └── composer │ │ ├── song.html │ │ └── index.html ├── views.py ├── models.py ├── composer.py └── trainer.py ├── .gitignore ├── requirements.txt ├── MANIFEST.in ├── README.md ├── LICENSE └── setup.py /composer/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea* 3 | db.* 4 | -------------------------------------------------------------------------------- /composer/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | -------------------------------------------------------------------------------- /composer/model_conf/chars.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhebrak/neural_composer/HEAD/composer/model_conf/chars.txt -------------------------------------------------------------------------------- /composer/model_conf/model_weights_34.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhebrak/neural_composer/HEAD/composer/model_conf/model_weights_34.h5 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | celery==3.1.20 2 | Django==1.92 3 | h5py==2.5.0 4 | music21==2.2.1 5 | keras==0.3.2 6 | redis==2.10.5 7 | requests==2.9.1 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include setup.py 3 | recursive-include composer/migrations * 4 | recursive-include composer/model_conf * 5 | recursive-include composer/static * 6 | recursive-include composer/templates * 7 | -------------------------------------------------------------------------------- /composer/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import numpy as np 4 | 5 | 6 | def sample(a, temperature): 7 | a = np.log(a) / temperature 8 | a = np.exp(a) / np.sum(np.exp(a)) 9 | 10 | return np.argmax(np.random.multinomial(1, a, 1)) 11 | -------------------------------------------------------------------------------- /composer/urls.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.conf.urls import url 4 | 5 | import views 6 | 7 | 8 | urlpatterns = [ 9 | url(r'^$', views.index, name='composer_index'), 10 | url(r'^song/compose/$', views.compose_song, name='composer_compose_song'), 11 | url(r'^song/(?P.+)/$', views.song, name='composer_song'), 12 | ] 13 | -------------------------------------------------------------------------------- /composer/admin.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.contrib import admin 4 | from django.core.urlresolvers import reverse 5 | 6 | from models import Song 7 | 8 | 9 | class SongAdmin(admin.ModelAdmin): 10 | list_display = ['key', 'is_composed', 'created_at', 'url'] 11 | 12 | def url(self, obj): 13 | return 'Link'.format(reverse('composer_song', kwargs={'key': obj.key})) 14 | 15 | url.allow_tags = True 16 | 17 | admin.site.register(Song, SongAdmin) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Using over 20k tunes in ABC notation from https://thesession.org/ and awesome deep learning library called [Keras](http://keras.io/) I've trained a multi-layer recurrent neural model that can build some melodies from scratch predicting exactly the next character in the text representation of a song. The output of this model then gets converted to MP3 format so it can be played in a browser right away. 2 | 3 | You can try it here — https://zhebrak.io/composer/ 4 | 5 | There is also a Telegram bot by [AyumuKasuga](https://github.com/AyumuKasuga/composerbot) — https://telegram.me/SongComposerBot 6 | -------------------------------------------------------------------------------- /composer/fetcher.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import requests as r 4 | 5 | from settings import MODEL_ABC_DATA_PATH 6 | 7 | 8 | URL = 'https://thesession.org/tunes/{}/abc' 9 | MAX_ID = 15240 10 | wrong_line_set = {'Z:', 'S:', 'R:', 'L:'} 11 | 12 | 13 | with open(MODEL_ABC_DATA_PATH, 'w') as f: 14 | for idx in range(1, MAX_ID): 15 | url = URL.format(idx) 16 | resp = r.get(url) 17 | if resp.status_code == 200: 18 | for line in resp.content.split('\n'): 19 | if not line[:2] in wrong_line_set: 20 | f.write(line + '\n') 21 | -------------------------------------------------------------------------------- /composer/writer.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | 5 | import celery 6 | import music21 7 | 8 | from models import Song 9 | 10 | 11 | @celery.task 12 | def write(song_key): 13 | song = Song.objects.get(key=song_key) 14 | 15 | score = music21.converter.parse(song.song) 16 | midi = music21.midi.translate.streamToMidiFile(score) 17 | 18 | if not os.path.exists(song.store_path): 19 | os.makedirs(song.store_path) 20 | 21 | midi.open(song.midi_file, 'wb') 22 | midi.write() 23 | midi.close() 24 | 25 | os.system('timidity -Ow -o - {} | lame - {}'.format(song.midi_file, song.mp3_file)) 26 | -------------------------------------------------------------------------------- /composer/settings.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | 5 | 6 | MEMORY_LENGTH = 42 7 | 8 | MAX_SONG_LENGTH = 300 9 | 10 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | MODEL_CONF_DIR = os.path.join(BASE_DIR, 'model_conf') 13 | MODEL_ARC_FILE = os.path.join(MODEL_CONF_DIR, 'model_arc.json') 14 | MODEL_ABC_DATA_PATH = os.path.join(MODEL_CONF_DIR, 'session_abc_data.txt') 15 | MODEL_CHARS_PATH = os.path.join(MODEL_CONF_DIR, 'chars.txt') 16 | 17 | MODEL_ARC_PATH = os.path.join(MODEL_CONF_DIR, MODEL_ARC_FILE) 18 | MODEL_WEIGHTS_PATH = os.path.join(MODEL_CONF_DIR, 'model_weights_34.h5') 19 | 20 | SONG_STORE_PATH = '/var/composer/media/' 21 | -------------------------------------------------------------------------------- /composer/migrations/0002_song_created_at.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-28 14:39 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | from django.db import migrations, models 7 | from django.utils.timezone import utc 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('composer', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='song', 19 | name='created_at', 20 | field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 2, 28, 14, 39, 43, 345106, tzinfo=utc), verbose_name='\u0434\u0430\u0442\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f'), 21 | preserve_default=False, 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /composer/templates/composer/song.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Song 6 | 7 | 8 | 9 | 10 |
11 | {% if song.is_composed %} 12 |
{{ song.song|safe }}
13 | 17 | {% else %} 18 | Composing... 19 | {% endif %} 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /composer/views.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | 5 | from django.shortcuts import redirect, render_to_response, get_object_or_404, RequestContext 6 | from django.core.urlresolvers import reverse 7 | 8 | import composer 9 | import writer 10 | 11 | from models import Song 12 | 13 | 14 | def index(request): 15 | return render_to_response('composer/index.html', { 16 | 'song_count': Song.objects.count() 17 | }) 18 | 19 | 20 | def compose_song(requset): 21 | song = composer.compose() 22 | return redirect(reverse('composer_song', kwargs={'key': song.key})) 23 | 24 | 25 | def song(request, key): 26 | song = get_object_or_404(Song.objects.all(), key=key) 27 | if not os.path.isfile(song.mp3_file) and song.is_composed: 28 | writer.write(key) 29 | 30 | return render_to_response('composer/song.html', { 31 | 'song': song, 32 | }, RequestContext(request)) 33 | -------------------------------------------------------------------------------- /composer/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-28 11:35 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Song', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('key', models.CharField(max_length=10, unique=True, verbose_name='\u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0435\u0441\u043d\u0438')), 21 | ('song', models.TextField(blank=True, null=True, verbose_name='\u043f\u0435\u0441\u043d\u044f')), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Alexander Zhebrak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | 5 | from distutils.core import setup 6 | 7 | 8 | __version__ = '0.01' 9 | 10 | short_description = 'Composing music with pre-trained neural network.' 11 | 12 | try: 13 | import pypandoc 14 | 15 | long_description = pypandoc.convert('README.md', 'rst') 16 | except (IOError, ImportError): 17 | long_description = short_description 18 | 19 | 20 | install_requires = [ 21 | 'Django>=1.9', 22 | 'celery>=3.1.20', 23 | 'h5py>=2.5.0', 24 | 'music21>=2.2.1', 25 | 'keras>=0.3.2', 26 | 'redis>=2.10.5', 27 | 'requests>=2.9.1' 28 | ] 29 | 30 | setup( 31 | name='neural_composer', 32 | packages=['composer'], 33 | version=__version__, 34 | description=short_description, 35 | long_description=long_description, 36 | author='Alexander Zhebrak', 37 | author_email='fata2ex@gmail.com', 38 | license='MIT', 39 | url='https://github.com/fata1ex/neural_composer', 40 | download_url='https://github.com/fata1ex/neural_composer', 41 | keywords=['django', 'neural network', 'music'], 42 | install_requires=install_requires, 43 | zip_safe=False, 44 | include_package_data=True, 45 | classifiers=[], 46 | ) 47 | -------------------------------------------------------------------------------- /composer/models.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import random 5 | import hashlib 6 | 7 | from django.db import models 8 | 9 | from settings import SONG_STORE_PATH 10 | 11 | from django.conf import settings 12 | 13 | 14 | class Song(models.Model): 15 | KEY_LENGTH = 10 16 | 17 | key = models.CharField(max_length=10, unique=True, verbose_name=u'уникальный ключ песни') 18 | song = models.TextField(blank=True, null=True, verbose_name=u'песня') 19 | created_at = models.DateTimeField(auto_now_add=True, verbose_name=u'дата создания') 20 | 21 | def __unicode__(self): 22 | return self.key 23 | 24 | @classmethod 25 | def create(cls): 26 | key = hashlib.md5(str(random.random())).hexdigest()[:cls.KEY_LENGTH] 27 | 28 | song = Song(key=key) 29 | song.save() 30 | 31 | return song 32 | 33 | @property 34 | def is_composed(self): 35 | return bool(self.song) 36 | 37 | @property 38 | def store_path(self): 39 | return os.path.join(SONG_STORE_PATH, self.created_at.strftime('%Y-%m-%d'), self.key) 40 | 41 | @property 42 | def mp3_file(self): 43 | return os.path.join(self.store_path, 'song.mp3') 44 | 45 | @property 46 | def midi_file(self): 47 | return os.path.join(self.store_path, 'song.mid') 48 | 49 | @property 50 | def mp3_media(self): 51 | return self.mp3_file.replace(SONG_STORE_PATH, settings.MEDIA_URL) 52 | -------------------------------------------------------------------------------- /composer/model_conf/model_arc.json: -------------------------------------------------------------------------------- 1 | {"layers": [{"name": "LSTM", "custom_name": "lstm", "inner_activation": "hard_sigmoid", "go_backwards": false, "output_dim": 128, "trainable": true, "input_shape": [42, 142], "stateful": false, "cache_enabled": true, "init": "glorot_uniform", "inner_init": "orthogonal", "input_dim": 142, "return_sequences": true, "activation": "tanh", "forget_bias_init": "one", "input_length": null}, {"cache_enabled": true, "trainable": true, "name": "Dropout", "custom_name": "dropout", "p": 0.2}, {"name": "LSTM", "custom_name": "lstm", "inner_activation": "hard_sigmoid", "go_backwards": false, "output_dim": 128, "trainable": true, "stateful": false, "cache_enabled": true, "init": "glorot_uniform", "inner_init": "orthogonal", "input_dim": 128, "return_sequences": false, "activation": "tanh", "forget_bias_init": "one", "input_length": null}, {"cache_enabled": true, "trainable": true, "name": "Dropout", "custom_name": "dropout", "p": 0.2}, {"W_constraint": null, "b_constraint": null, "name": "Dense", "custom_name": "dense", "activity_regularizer": null, "trainable": true, "cache_enabled": true, "init": "glorot_uniform", "activation": "linear", "input_dim": null, "b_regularizer": null, "W_regularizer": null, "output_dim": 142}, {"cache_enabled": true, "activation": "softmax", "trainable": true, "name": "Activation", "custom_name": "activation"}], "optimizer": {"epsilon": 1e-06, "lr": 0.0010000000474974513, "name": "RMSprop", "rho": 0.8999999761581421}, "class_mode": "categorical", "name": "Sequential", "loss": "categorical_crossentropy"} -------------------------------------------------------------------------------- /composer/templates/composer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Neural Composer 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Songs composed: {{ song_count }}

13 | 14 |
15 | Composed songs: #1, #2 16 |
17 |
18 |

19 | Using over 20k tunes in ABC notation from The Session and awesome deep learning library called Keras I've trained a multi-layer recurrent neural model that can build some melodies from scratch predicting exactly the next character in the text representation of a song. The output of this model then gets converted to MP3 format so it can be played in a browser right away. 20 |

21 |
22 |

23 | There is also a Telegram bot by AyumuKasuga 24 |

25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /composer/composer.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import random 4 | import numpy as np 5 | 6 | import celery 7 | 8 | from keras.models import model_from_json 9 | 10 | from models import Song 11 | 12 | import utils 13 | import writer 14 | 15 | from settings import MEMORY_LENGTH, MAX_SONG_LENGTH, MODEL_ARC_PATH, MODEL_CHARS_PATH, MODEL_WEIGHTS_PATH 16 | 17 | 18 | class WriterException(Exception): 19 | pass 20 | 21 | 22 | model = None 23 | 24 | 25 | def get_model(): 26 | global model 27 | 28 | if model is not None: 29 | return model 30 | 31 | model = model_from_json(open(MODEL_ARC_PATH, 'r').read()) 32 | model.load_weights(MODEL_WEIGHTS_PATH) 33 | 34 | model.chars = set(open(MODEL_CHARS_PATH, 'r').read()) 35 | model.char_indices = dict((c, i) for i, c in enumerate(model.chars)) 36 | model.indices_char = dict((i, c) for i, c in enumerate(model.chars)) 37 | 38 | return model 39 | 40 | 41 | def compose(): 42 | song = Song.create() 43 | compose_async.delay(song.key) 44 | return song 45 | 46 | 47 | @celery.task 48 | def compose_async(song_key): 49 | model = get_model() 50 | 51 | while True: 52 | diversity = random.uniform(0.7, 1.0) 53 | sentence = '#' * MEMORY_LENGTH + 'X:' 54 | sentence = sentence[-MEMORY_LENGTH:] 55 | generated = 'X:' 56 | 57 | while True: 58 | x = np.zeros((1, MEMORY_LENGTH, len(model.chars))) 59 | for t, char in enumerate(sentence): 60 | x[0, t, model.char_indices[char]] = 1. 61 | 62 | preds = model.predict(x, verbose=0)[0] 63 | next_index = utils.sample(preds, diversity) 64 | next_char = model.indices_char[next_index] 65 | 66 | sentence = sentence[-MEMORY_LENGTH + 1:] + next_char 67 | generated += next_char 68 | 69 | if generated.endswith('$$$'): 70 | try: 71 | song = Song.objects.get(key=song_key) 72 | song.song = generated.rstrip('$') 73 | song.save() 74 | 75 | writer.write(song_key) 76 | except WriterException: 77 | break 78 | else: 79 | return 80 | 81 | if len(generated) > MAX_SONG_LENGTH: 82 | break 83 | -------------------------------------------------------------------------------- /composer/trainer.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import random 7 | 8 | import numpy as np 9 | 10 | from keras.models import Sequential 11 | from keras.layers.core import Dense, Activation, Dropout 12 | from keras.layers.recurrent import LSTM 13 | 14 | from utils import sample 15 | from settings import MODEL_CONF_DIR, MODEL_ARC_FILE, MODEL_ABC_DATA_PATH, MEMORY_LENGTH, MODEL_CHARS_PATH 16 | 17 | 18 | learning_batch_size = 10**6 19 | hidden_layer_size = 128 20 | dropout_size = 0.2 21 | 22 | 23 | song_list = [] 24 | 25 | with open(MODEL_ABC_DATA_PATH, 'r') as f: 26 | tmp_song = '' 27 | for line in f.readlines(): 28 | if line.startswith('X:'): 29 | song_list.append(tmp_song.strip()) 30 | tmp_song = '' 31 | 32 | tmp_song += line 33 | 34 | song_list = song_list[1:] 35 | print('song count:', len(song_list)) 36 | 37 | text = open(MODEL_ABC_DATA_PATH, 'r').read() 38 | chars = sorted(list(set(text))) 39 | open(MODEL_CHARS_PATH, 'w').write(''.join(chars)) 40 | 41 | char_indices = dict((c, i) for i, c in enumerate(chars)) 42 | indices_char = dict((i, c) for i, c in enumerate(chars)) 43 | 44 | sentences = [] 45 | next_chars = [] 46 | 47 | for song in song_list: 48 | song = '#' * MEMORY_LENGTH + song + '$' * 7 49 | for _ in range(0, learning_batch_size / len(song_list)): 50 | start_index = random.randint(0, len(song) - MEMORY_LENGTH - 1) 51 | sentences.append(song[start_index:start_index + MEMORY_LENGTH]) 52 | next_chars.append(song[start_index + MEMORY_LENGTH]) 53 | 54 | print('sentences count:', len(sentences)) 55 | 56 | X = np.zeros((len(sentences), MEMORY_LENGTH, len(chars)), dtype=np.bool) 57 | y = np.zeros((len(sentences), len(chars)), dtype=np.bool) 58 | 59 | for i, sentence in enumerate(sentences): 60 | for t, char in enumerate(sentence): 61 | X[i, t, char_indices[char]] = 1 62 | y[i, char_indices[next_chars[i]]] = 1 63 | 64 | 65 | print('building a model...') 66 | model = Sequential() 67 | model.add(LSTM(hidden_layer_size, return_sequences=True, input_shape=(MEMORY_LENGTH, len(chars)))) 68 | model.add(Dropout(dropout_size)) 69 | model.add(LSTM(hidden_layer_size, return_sequences=False)) 70 | model.add(Dropout(dropout_size)) 71 | model.add(Dense(len(chars))) 72 | model.add(Activation('softmax')) 73 | 74 | model.compile(loss='categorical_crossentropy', optimizer='rmsprop') 75 | model_path = '{}/h{}_m{}_d{}'.format(MODEL_CONF_DIR, str(hidden_layer_size), str(MEMORY_LENGTH), str(dropout_size)) 76 | os.mkdir(model_path) 77 | open('{}/{}'.format(model_path, MODEL_ARC_FILE), 'w').write(model.to_json()) 78 | 79 | 80 | for iteration in range(1, 128): 81 | print('iteration:', iteration) 82 | model.fit(X, y, batch_size=128, nb_epoch=1) 83 | 84 | iteration_song_list = [] 85 | for _ in range(10): 86 | diversity = random.randint(5, 10) / 10.0 87 | sentence = '#' * MEMORY_LENGTH + 'X:' 88 | sentence = sentence[-MEMORY_LENGTH:] 89 | generated = 'X:' 90 | 91 | while True: 92 | x = np.zeros((1, MEMORY_LENGTH, len(chars))) 93 | for t, char in enumerate(sentence): 94 | x[0, t, char_indices[char]] = 1. 95 | 96 | preds = model.predict(x, verbose=0)[0] 97 | next_index = sample(preds, diversity) 98 | next_char = indices_char[next_index] 99 | 100 | sentence = sentence[-MEMORY_LENGTH + 1:] + next_char 101 | generated += next_char 102 | 103 | if len(generated) > 400 or generated.endswith('$$$'): 104 | break 105 | 106 | iteration_song_list.append(generated.rstrip('$')) 107 | 108 | model.save_weights('{}/model_weights_{}.h5'.format(model_path, iteration)) 109 | with open('{}/songs_{}.txt'.format(model_path, iteration), 'w') as f: 110 | for song in iteration_song_list: 111 | f.write(song + '\n\n') 112 | --------------------------------------------------------------------------------