Lyricize: une application Flask pour créer des paroles à l’aide de chaînes de Markov

Lyricize: une application Flask pour créer des paroles à l’aide de chaînes de Markov

Nouveaux codeurs are //www.reddit.com/r/learnpython/comments/ul1b8/any_good_projects_for_beginners/[looking http://www.reddit.com/r/learnpython/comments/1a9yie/worked_with_python_for_almost_a_year_now_how_to_. reddit.com/r/learnpython/comments/1iqv3c/please_help_me_prepare_a_roadmap_as_to_what_i/[new] http://www.reddit.com/r/learnpython/comments/1czbfw/kinda_lostwhat_to_do_next/ainsi que des projets] -! Non seulement créer votre propre side project est le meilleur moyen d’acquérir une expérience pratique, mais si vous cherchez à passer d’un passe-temps à une profession, puis les projets parallèles sont un excellent moyen de commencer à construire un portefeuille de travail.

De l’idée au MVP

Dans cet article, nous allons aborder le processus de lancement d’un MVP (strict minimum), de la conception initiale de l’idée à un prototype partageable. À la fin, vous aurez créé votre propre version de http://lyricize.herokuapp.com [Lyricize], une petite application qui utilise les paroles d’un artiste ou d’un groupe pour générer de «nouvelles» paroles au son similaire basées sur des probabilités. Au lieu de présenter le didacticiel type "Voici comment reproduire tout ce code", nous allons parcourir le processus étape par étape pour montrer ce qui est réellement impliqué dans le processus de réflexion et la création en cours de route.

_ Notez qu’il ne s’agit pas nécessairement de construire le prochain démarrage de tueur; nous cherchons simplement à trouver un projet qui peut être 1) une occasion d’apprentissage amusante et 2) partageable avec les autres. _

*Avant de commencer, jetez un œil à l'exemple http://lyricize.herokuapp.com [app] pour voir ce que vous allez créer.* Essentiellement, vous pouvez générer de nouvelles paroles en fonction des paroles d'un artiste particulier à l'aide des chaînes Markov. Par exemple, essayez de rechercher «Bob Dylan» et remplacez le nombre de lignes par trois. Assez cool, non? Je viens d'exécuter la même recherche, qui a abouti à:
*Yonder tient votre promesse et tous les bateaux* + *Je suis prêt pour la gorge* + *Je suis un crochet*

Si profond. Quoi qu’il en soit, commençons…

Trouver un sujet d’intérêt

Donc, étape 1: Trouvez un sujet sur lequel vous souhaitez en savoir plus. L’application suivante a été inspirée par une ancienne college assignation (certes pas la source d’inspiration la plus courante) qui utilise Markov chaînes pour générer un texte «réel» avec un corps d’exemple de texte. Les modèles de Markov apparaissent dans http://en.wikipedia.org/wiki/Markov_chain#Applications par toutes sortes de scénarios]. (Nous allons nous plonger dans ce qu’est un modèle de Markov sous peu.) J’ai trouvé l’idée de la génération de texte basée sur les probabilités particulièrement intéressante; en particulier, je me demandais ce qui se passerait si vous utilisiez des paroles de chanson comme exemple de texte pour générer de «nouvelles» paroles…

A l’Internet! Une recherche rapide sur le Web montre quelques sites de générateurs de paroles basés sur Markov, mais rien de tel que ce que j’ai en tête. De plus, brancher le code complet de quelqu’un d’autre n’est pas un moyen très efficace d’apprendre comment fonctionnent réellement les générateurs Markov; construisons le nôtre.

Alors… comment fonctionnent les générateurs Markov? Fondamentalement, une chaîne de Markov est générée à partir de certains textes en fonction de la fréquence de certains modèles. Par exemple, considérons la chaîne suivante comme exemple de texte:

bobby

Nous construirons le modèle de Markov le plus simple possible à partir de ce texte, qui est un modèle de Markov de ordre 0, afin de prédire la probabilité qu’une lettre particulière se produise. Il s’agit d’un tableau de fréquences simple:

b: 3/5
o: 1/5
y: 1/5

Cependant, c’est un assez mauvais modèle de langage; en plus de la fréquence à laquelle les lettres se produisent globalement, nous voulons également examiner la fréquence à laquelle une lettre particulière se produit _ étant donné la lettre précédente_. Comme nous dépendons d’une lettre précédente, il s’agit d’un modèle de Markov d’ordre 1:

given "b":
  "b" is next: 1/3
  "o" is next: 1/3
  "y" is next: 1/3
given "o":
  "b" is next: 1
given "y":
  [terminates]: 1

De là, vous pourriez imaginer des modèles de Markov d’ordre supérieur; un modèle d’ordre 2 commencerait par mesurer la fréquence de chaque lettre qui se produit après la chaîne de deux lettres «bo», etc. En augmentant l’ordre, nous obtenons un modèle qui commence à ressembler davantage à du vrai langage; par exemple, un modèle de Markov d’ordre 5 qui avait reçu de nombreux exemples de saisie, y compris le mot «python», serait très susceptible de suivre la chaîne «pytho» avec un «n», alors qu’un modèle d’ordre beaucoup plus faible aurait pu proposer quelques mots créatifs.

Commencez à développer

Comment procéder pour construire une approximation approximative d’un modèle de Markov? Essentiellement, la structure que nous avons décrite ci-dessus avec les modèles d’ordre supérieur est un dictionnaire de dictionnaires. Vous pourriez imaginer un dictionnaire + modèle + avec divers fragments de mots (c’est-à-dire, "bo") comme clés. Chacun de ces fragments pointerait alors à son tour vers un dictionnaire, ces dictionnaires internes contenant les lettres individuelles suivantes («y») comme clés avec leurs fréquences respectives comme valeurs.

Commençons par créer une méthode + generateModel () + qui prend un exemple de texte et un ordre de modèle de Markov, puis retourne ce dictionnaire de dictionnaires:

def generateModel(text, order):
    model = {}
    for i in range(0, len(text) - order):
        fragment = text[i:i+order]
        next_letter = text[i+order]
        if fragment not in model:
            model[fragment] = {}
        if next_letter not in model[fragment]:
            model[fragment][next_letter] = 1
        else:
            model[fragment][next_letter] += 1
    return model

Nous avons parcouru tout le texte disponible, en remontant jusqu’au dernier fragment complet disponible + lettre suivante afin de ne pas exécuter la fin de la chaîne, en ajoutant nos dictionnaires + fragment + au + modèle + avec chaque + fragment + tenant un dictionnaire des fréquences totales + next_letter +.

Copiez cette fonction dans un shell Python et essayez-la:

>>>

>>> generateModel("bobby", 1)
{'b': {'y': 1, 'b': 1, 'o': 1}, 'o': {'b': 1}}

Ça fera l’affaire! Nous avons des nombres de fréquences au lieu de probabilités relatives, mais nous pouvons travailler avec cela; il n’y a aucune raison de normaliser chaque dictionnaire pour augmenter les probabilités de 100%.

Utilisons maintenant ce + modèle + dans une méthode + getNextCharacter () + qui, compte tenu d’un modèle et d’un fragment, décidera d’une lettre suivante appropriée compte tenu des probabilités du modèle:

from random import choice
def getNextCharacter(model, fragment):
    letters = []
    for letter in model[fragment].keys():
        for times in range(0, model[fragment][letter]):
            letters.append(letter)
    return choice(letters)

Ce n’est pas la configuration la plus efficace, mais elle est simple à construire et fonctionne pour l’instant. Nous avons simplement construit une liste de lettres, compte tenu de leur fréquence totale d’occurrence après le fragment, et choisi au hasard dans cette liste.

Il ne reste plus qu’à utiliser ces deux méthodes dans une troisième méthode qui va réellement générer du texte d’une certaine longueur spécifiée. Pour ce faire, nous devons garder une trace du fragment de texte actuel que nous construisons tout en ajoutant de nouveaux caractères:

def generateText(text, order, length):
    model = generateModel(text, order)

    currentFragment = text[0:order]
    output = ""
    for i in range(0, length-order):
        newCharacter = getNextCharacter(model, currentFragment)
        output += newCharacter
        currentFragment = currentFragment[1:] + newCharacter
    print output

Faisons-en un script exécutable complet qui prend un ordre de Markov et affiche la longueur du texte comme arguments:

from random import choice
import sys

def generateModel(text, order):
    model = {}
    for i in range(0, len(text) - order):
        fragment = text[i:i+order]
        next_letter = text[i+order]
        if fragment not in model:
            model[fragment] = {}
        if next_letter not in model[fragment]:
            model[fragment][next_letter] = 1
        else:
            model[fragment][next_letter] += 1
    return model

def getNextCharacter(model, fragment):
    letters = []
    for letter in model[fragment].keys():
        for times in range(0, model[fragment][letter]):
            letters.append(letter)
    return choice(letters)

def generateText(text, order, length):
    model = generateModel(text, order)
    currentFragment = text[0:order]
    output = ""
    for i in range(0, length-order):
        newCharacter = getNextCharacter(model, currentFragment)
        output += newCharacter
        currentFragment = currentFragment[1:] + newCharacter
    print output

text = "some sample text"
if __name__ == "__main__":
    generateText(text, int(sys.argv[1]), int(sys.argv[2]))

Pour l’instant, nous allons générer un exemple de texte via la méthode très scientifique de jeter une chaîne directement dans le code sur la base de quelques paroles copiées et collées d’Alanis Morisette.

Test

Enregistrez le script et donnez-lui un tourbillon:

$ python markov.py 2 100
I wounts
You ho's humortel whime
 mateend I wass
How by Lover
$ python markov.py 4 100
stress you to cosmic tears
All they've cracked you (honestly) at the filler in to like raise
$ python markov.py 6 100
tress you place the wheel from me
Please be philosophical
Please be tapped into my house

Eh bien, c’était juste précieux. Les deux derniers essais sont assez représentatifs de ses paroles (bien que le premier échantillon de l’ordre 2 ressemble plus à Björk). Ces résultats sont suffisamment encourageants pour une esquisse de code rapide, alors transformons cette chose en un vrai projet.

Prochaine itération

Premier obstacle: comment allons-nous automatiser l’obtention de nombreuses paroles? Une option serait de gratter sélectivement le contenu d’un site de paroles, mais cela ressemble à beaucoup d’efforts pour des résultats probablement de faible qualité, plus une zone grise légale potentielle étant donné l’ombre de la plupart des agrégateurs de paroles et le draconianisme de l’industrie musicale. Voyons plutôt s’il existe des API ouvertes. En allant sur programmableweb.com, nous trouvons en fait 14 différentes API de paroles répertoriées. Cependant, ces listes ne sont pas toujours les plus à jour. Par conséquent, recherchons les plus récentes.

LYRICSnMUSIC propose gratuitement une RESTful API en utilisant JSON pour renvoyer jusqu’à 150 caractères de paroles de chanson. Cela semble parfait pour notre cas d’utilisation, surtout compte tenu de la répétition de la plupart des chansons; il n’est pas nécessaire de rassembler les paroles complètes lorsqu’un seul échantillon suffira. Allez chercher une new key pour pouvoir accéder à leur API.

Essayons leur API avant de nous installer définitivement sur cette source. Sur la base de leur documentation, nous pouvons faire un exemple de demande comme suit:

http://api.lyricsnmusic.com/songs?api_key=[YOUR_API_KEY_HERE]&artist=coldplay

Les résultats JSON qu’il crache dans un navigateur sont un peu difficiles à lire; à travers eux dans un formatter pour mieux voir. Il semble que nous récupérions avec succès une liste de dictionnaires basés sur les chansons de Coldplay:

[
  {
     "title":"Don't Panic",
     "url":"http://www.lyricsnmusic.com/coldplay/don-t-panic-lyrics/4294612",
     "snippet":"Bones sinking like stones \r\nAll that we've fought for \r\nHomes, places we've grown \r\nAll of us are done for \r\n\r\nWe live in a beautiful world \r\nYeah we ...",
     "context":null,
     "viewable":true,
     "instrumental":false,
     "artist":{
        "name":"Coldplay",
        "url":"http://www.lyricsnmusic.com/coldplay"
     }
  },
  {
     "title":"Shiver",
     "url":"http://www.lyricsnmusic.com/coldplay/shiver-lyrics/4294613",
     "snippet":"So I look in your direction\r\nBut you pay me no attention, do you\r\nI know you don't listen to me\r\n'Cause you say you see straight through me, don't you...",
     "context":null,
     "viewable":true,
     "instrumental":false,
     "artist":{
        "name":"Coldplay",
        "url":"http://www.lyricsnmusic.com/coldplay"
     }
  },
  ...
]

Il n’y a aucun moyen de limiter la réponse, mais nous ne sommes intéressés que par chaque "extrait" fourni, ce qui convient parfaitement à ce projet.

Nos expériences préliminaires avec les générateurs Markov étaient éducatives, mais notre modèle actuel n’est pas le mieux adapté à la tâche de génération de paroles. D’une part, nous devrions probablement utiliser des mots individuels comme jetons plutôt que de prendre les choses caractère par caractère; c’est amusant d’essayer de se moquer de la langue elle-même, mais pour générer de fausses paroles, nous voudrons nous en tenir au vrai anglais. Cela semble plus délicat, cependant, et nous avons parcouru un long chemin pour comprendre le fonctionnement des chaînes de Markov, ce qui était l’objectif initial de cet exercice. À ce stade, nous atteignons un carrefour: réinventer la roue métaphorique pour plus d’apprentissage (cela pourrait être une excellente pratique de codage), ou voir ce que d’autres ont déjà créé.

J’ai choisi la sortie paresseuse et je suis retourné pour fouiller les toiles intérieures. Une âme gentille sur GitHub a déjà implemented une chaîne de Markov de base basée sur un seul mot et l’a même téléchargée sur PyPI. En parcourant rapidement le code, il apparaît que ce modèle n’est que de l’ordre 0. Cela aurait probablement été assez rapide à construire par nous-mêmes, alors qu’un modèle d’ordre supérieur pourrait représenter beaucoup plus de travail. Pour l’instant, allons-y avec la roue pré-emballée de quelqu’un d’autre; au moins un modèle d’ordre 0 ne finira pas par ressembler à Björk si nous utilisons des mots entiers.

Puisque nous voulons partager facilement notre création avec des amis et la famille, il est logique de la transformer en application Web. Maintenant, pour choisir un framework web. Personnellement, je suis de loin le plus familier avec Django, mais cela semble exagéré ici; après tout, nous n’aurons même pas besoin de notre propre base de données. Essayons Flask.

Ajouter une fiole

Conformément à la routine habituelle, https://realpython.com/python-virtual-environments-a-primer/{fire un environnement virtuel] - si vous ne l’avez pas déjà fait! Si ce n’est pas un processus familier, jetez un œil à certains de nos publications précédentes pour savoir comment se mettre en place vers le haut.

$ mkdir lyricize
$ cd lyricize
$ virtualenv --no-site-packages venv
$ source venv/bin/activate

Toujours comme d’habitude, installez les exigences nécessaires et jetez-les dans un fichier requirements.txt:

$ pip install PyMarkovChain flask requests
$ pip freeze > requirements.txt

Nous avons également ajouté la bibliothèque requests afin de pouvoir effectuer des requêtes Web auprès de l’API de paroles.

Maintenant, pour faire l’application. Par souci de simplicité, divisons-le en deux pages: la page principale présentera à l’utilisateur un formulaire de base pour choisir un nom d’artiste et un certain nombre de lignes de paroles à générer, tandis qu’une deuxième page «paroles» présentera le résultats. Commençons par une application barebones Flask nommée app.py qui utilise un modèle index.html :

from flask import Flask, render_template

app = Flask(__name__)
app.debug = True

@app.route('/', methods=['GET'])
def index():
    return render_template('index.html')

if __name__ == '__main__':
    app.run()

Toute cette application va jusqu’à présent charger le contenu d’un modèle index.html. Faisons-en une forme de base:

<html>
 <body>
  <form action="#" method="post" class="lyrics">
    Artist or band name: <input name="artist" type="text"/><br/>
    Number of lines:
    <select name="lines">
      {% for n in range(1,11) %}
        <option value="{{n}}">{{n}}</option>
      {% endfor %}
    </select>
    <br/><br/>
    <input class="button" type="submit" value="Lyricize">
  </form>
 </body>
</html>

Enregistrez ce index.html dans un dossier séparé nommé templates afin que Flask puisse le trouver. Ici, nous utilisons le modèle Jinja2 de Flask pour créer une liste déroulante de "sélection" basée sur une boucle couvrant les nombres 1 à 10. Avant d’ajouter autre chose, lancez cette page pour vous assurer que nous sommes correctement configurés:

$ python app.py
 *Running on http://127.0.0.1:5000/

Vous devriez maintenant pouvoir visiter http://127.0.0.1:5000/dans un navigateur et voir le joli formulaire.

Décidons maintenant ce que nous voulons afficher sur la page de résultats, afin de savoir ce que nous devons lui transmettre:

<html>
 <body>
  <div align="center" style="padding-top:20px;">
   <h2>
   {% for line in result %}
     {{ line }}<br/>
   {% endfor %}
   </h2>
   <h3>{{ artist }}</h3>
   <br/>
   <form action="{{ url_for('index') }}">
    <input type="submit" value="Do it again!"/>
   </form>
  </div>
 </body>
</html>

Ici, nous avons parcouru un tableau* result , ligne par ligne, en affichant chaque ligne séparément. Ci-dessous, nous montrons *l’artiste sélectionné et un lien vers la page d’accueil. Enregistrez-le sous lyrics.html dans votre répertoire/templates.

Nous devons également mettre à jour l’action de formulaire de index.html pour pointer vers cette page de résultats:

<form action="{{ url_for('lyrics') }}" method="post" class="lyrics">

Maintenant, pour écrire un itinéraire pour la page de paroles résultante:

@app.route('/lyrics', methods=['POST'])
def lyrics():
    artist = request.form['artist']
    lines = int(request.form['lines'])

    if not artist:
        return redirect(url_for('index'))

    return render_template('lyrics.html', result=['hello', 'world'], artist=artist)

Cette page prend une demande POST à ​​partir du formulaire, analysant l’artiste et le nombre de lignes fournis - nous ne générons pas encore de paroles, donnant simplement au modèle une liste fictive des résultats. Nous devrons également ajouter la fonctionnalité Flask nécessaire - + url_for + et `+ redirect + '- sur laquelle nous nous sommes appuyés:

from flask import Flask, render_template, url_for, redirect

Testez-le pour vous assurer que rien n’est encore cassé:

$ python app.py

Super, maintenant pour la vraie viande du projet. Dans les paroles (), récupérons une réponse de LYRICSnMUSIC basée sur notre paramètre d’artiste transmis:

# Get a response of sample lyrics from the provided artist
uri = "http://api.lyricsnmusic.com/songs"
params = {
    'api_key': API_KEY,
    'artist': artist,
}
response = requests.get(uri, params=params)
lyric_list = response.json()

À l’aide de requêtes, nous récupérons une URL spécifique qui comprend un dictionnaire de paramètres: le nom de l’artiste fourni et notre clé API. Cette clé d’API privée ne doit pas apparaître dans votre code; après tout, vous voudrez partager ce code avec d’autres. À la place, créons un fichier séparé pour conserver cette valeur en tant que variable:

$ echo "API_KEY=[youractualapikeygoeshere]" > .env

Nous avons créé un fichier "d’environnement" spécial que Flask peut désormais lire si nous ajoutons simplement les éléments suivants en haut de notre application:

import os
API_KEY = os.environ.get('API_KEY')

Et enfin, ajoutons la fonctionnalité de chaîne Markov. Maintenant que nous utilisons le package de quelqu’un d’autre, cela finit par être assez banal. Tout d’abord, ajoutez l’importation en haut:

from pymarkovchain import MarkovChain

Et puis, après avoir reçu une réponse paroles de l’API, nous créons simplement un MarkovChain, chargez les données de paroles et générez une liste de phrases:

mc = MarkovChain()
mc.generateDatabase(lyrics)

result = []
for line in range(0, lines):
    result.append(mc.generateString())

Au total, app.py devrait maintenant ressembler à ceci:

from flask import Flask, url_for, redirect, request, render_template
import requests
from pymarkovchain import MarkovChain
import os

API_KEY = os.environ.get('API_KEY')

app = Flask(__name__)
app.debug = True

@app.route('/', methods=['GET'])
def index():
    return render_template('index.html')

@app.route('/lyrics', methods=['POST'])
def lyrics():
    artist = request.form['artist']
    lines = int(request.form['lines'])

    if not artist:
        return redirect(url_for('index'))

    # Get a response of sample lyrics from the artist
    uri = "http://api.lyricsnmusic.com/songs"
    params = {
        'api_key': API_KEY,
        'artist': artist,
    }
    response = requests.get(uri, params=params)
    lyric_list = response.json()

    # Parse results into a long string of lyrics
    lyrics = ''
    for lyric_dict in lyric_list:
        lyrics += lyric_dict['snippet'].replace('...', '') + ' '

    # Generate a Markov model
    mc = MarkovChain()
    mc.generateDatabase(lyrics)

    # Add lines of lyrics
    result = []
    for line in range(0, lines):
        result.append(mc.generateString())

    return render_template('lyrics.html', result=result, artist=artist)

if __name__ == '__main__':
    app.run()

Essaye le! Tout devrait fonctionner localement. Maintenant, pour le partager avec le monde…

Déployer sur Heroku

Accueillons Heroku, car (pour ces exigences minimales) nous pouvons le faire gratuitement. Pour ce faire, nous devons apporter quelques modifications mineures au code. Tout d’abord, ajoutez un Procfile qui indiquera à Heroku comment servir l’application:

$ echo "web: python app.py" > Procfile

Ensuite, puisque Heroku spécifie un port aléatoire sur lequel exécuter l’application, vous devrez saisir un numéro de port en haut:

PORT = int(os.environ.get('PORT', 5000))

app = Flask(__name__)
app.config.from_object(__name__)

Et lorsque l’application est exécutée, assurez-vous de passer ce port

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=PORT)

Nous avons également dû spécifier l’hôte de «0.0.0.0» car Flask s’exécute par défaut en privé sur l’ordinateur local, tandis que nous voulons que l’application s’exécute sur Heroku sur une IP accessible au public.

Enfin, supprimez + app.debug = True + de votre code afin que les utilisateurs ne voient pas toutes vos erreurs de trace de pile en cas de problème.

Initialisez un référentiel git (si vous ne l’avez pas déjà fait), créez une nouvelle application Heroku, et poussez-y votre code!

$ git init
$ git add .
$ git commit -m "First commit"
$ heroku create
$ git push heroku master

Voir les Heroku docs pour un aperçu plus complet de ce processus de déploiement. Assurez-vous d’ajouter votre variable API_KEY sur Heroku:

$ heroku config:set API_KEY=[youractualapikeygoeshere]

Et nous sommes tous prêts! Il est temps de partager votre création avec le monde - ou de continuer à pirater :)

Conclusion et prochaines étapes

Si vous avez aimé ce contenu, vous pourriez être intéressé par nos https://realpython.com [cours actuels] pour l’apprentissage du développement web ou notre plus récent Kickstarter qui couvre des techniques plus avancées. Ou - jouez simplement avec l’application http://lyricize.herokuapp.com [ici].

*Prochaines étapes possibles:*
  • Ce HTML semble avoir été écrit au début des années 90; utilisez Bootstrap ou juste quelques CSS de base pour le style

  • Ajoutez quelques commentaires au code avant d’oublier ce qu’il fait! (Ceci est laissé comme un exercice pour le lecteur: o)

  • Abstraire le code dans la route des paroles pour être des méthodes individuelles (c’est-à-dire une méthode pour retourner les réponses de l’API des paroles et une autre méthode distincte pour générer le modèle de Markov); cela rendra le code plus facile à maintenir et à tester à mesure qu’il grandit en taille et en complexité

  • Créer un générateur de Markov capable d’utiliser des ordres plus élevés

  • Utilisez Flask-WTF pour améliorer les formulaires et la validation des formulaires

  • En parlant de ça: rendez-le plus sûr! À l’heure actuelle, quelqu’un pourrait envoyer des demandes POST déraisonnables, injecter son propre code dans la page ou faire du DoS sur le site avec de nombreuses demandes rapidement répétées; ajouter une validation d’entrée solide et une limitation de débit de base

  • Ajoutez une meilleure gestion des erreurs; que faire si un appel API prend trop de temps ou échoue pour une raison quelconque?

  • Jetez les résultats dans un moteur text-to-speech, apprenez à varier les modèles de hauteur en utilisant un autre modèle de Markov et réglez sur un rythme; vous serez bientôt en tête des classements!