DEV Community

Cover image for App em Flask com IA/ML - desenvolvimento e deploy 100% free (PARTE 1)
msc2020
msc2020

Posted on • Updated on • Originally published at github.com

App em Flask com IA/ML - desenvolvimento e deploy 100% free (PARTE 1)

Neste tutorial, desenvolvemos um pequeno e simples aplicativo web utilizando ferramentas gratuitas. Usamos Python e recursos de aprendizagem de máquina (machine learning - ML) para realizar análises de suspeitas de plágio utilizando inteligência artificial (IA). Além disso, colocamos o app em produção, disponibilizando seu acesso para qualquer computador na web!

Vamos lá? ☕

1. O que vc acha do mini app abaixo?

imagem gif do app

Veja o app rodando em https://checaplagio-1-a4780033.deta.app/


💻 Reflexão: Como o app mostrado acima foi desenvolvido? Podemos criar nosso próprio app em Python usando tecnologias gratuitas (100% free)? Há uma maneira pratica de embutir funcionalidades com aprendizagem de máquina num app?

Se sua resposta foi sim para as três perguntas, talvez este post contribua mais para vc na parte do caso de estudo. Se vc está entre o talvez ou não como resposta em alguma delas, é bem provável que este post irá contribuir para sua bagagem em desenvolvimento e deploy de projetos com ferramentas Python.🍵

1.1. O que o app fará?

O web app que desenvolveremos e colocaremos em produção no Deta Space, serve para identificar se dois textos são "iguais" ou "não". Quem irá materializar isso que chamamos de "iguais", será um modelo de inteligência artificial (IA) gratuito e disponível no site da Hugging Face. Acessaremos o modelo de IA por meio de uma API (Application Programming Interface).

1.2. O que é feito neste tutorial (PARTE 1 e PARTE 2) ?

  • Configuração de um ambiente virtual com Python para rodar o micro-framework Flask (PARTE 1)

  • Desenvolvimento da parte lógica do app usando os padrões de templates, rotas e funções de visualização do Flask (PARTE 1)

  • Como embutir um modelo de aprendizagem de máquina (ML) no app (PARTE 1)

  • Realizar o deploy do app em um ambiente de produção (PARTE 2)

2. Como rodar localmente o app feito com o Flask?

No desenvolvimento de software em Python é comum o uso de um ambiente virtual (virtual environment, venv) para isolar as bibliotecas que são pré-requisitos. As bibliotecas Python e suas dependências costumam funcionar sem conflitos quando se usa um ambiente virtual específico durante o desenvolvimento.

2.1. Criação e ativação do ambiente virtual local

Primeiramente, vamos criar uma pasta/diretório com o nome-do-app:

mkdir nome-do-app
cd nome-do-app
Enter fullscreen mode Exit fullscreen mode

Fique à vontade para colocar o nome que desejar ao invés de nome-do-app. Esse será nosso diretório raiz.

Dentro da pasta criada (nome-do-app), criaremos o ambiente virtual. Para isso usamos o seguinte comando que deve ser digitado em um terminal (bash, shell ou prompt) que possua o Python 3 instalado:

python -m venv venv_app
Enter fullscreen mode Exit fullscreen mode

Após criar o ambiente virtual (venv_app), precisamos ativá-lo:

  • no Linux:
source venv_app/bin/activate
Enter fullscreen mode Exit fullscreen mode
  • no Windows:
 venv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode

Nota: Os arquivos que estão dentro de venv_app são gerados automaticamente no momento em que criamos o ambiente virtual. Portanto, não precisamos nos preocupar com eles.

2.2 Instalação das dependências

Agora que já estamos com o ambiente virtual local ativado, devemos instalar neste ambiente os pré-requisitos (requirements.txt) que serão usados.

O conteúdo do arquivo requirements.txt é:

# requirements.txt

Flask
Flask-Bootstrap # extensão para bootstrap (css, styles, navbar, etc)
Flask-Moment # extensão para manipular datetime
Flask-WTF # extensão para web forms
python-dotenv # gerenciador de variáveis env
requests
waitress
Enter fullscreen mode Exit fullscreen mode

Para instalar no Python as bibliotecas desse arquivo .txt digitamos o seguinte comando no terminal:

pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Estrutura dos arquivos até o momento:

nome-do-app/
│
├── venv_app/
│
└── requirements.txt
Enter fullscreen mode Exit fullscreen mode

3. Primeira versão do app

Seguindo, mais ou menos, a documentação do Flask, agora criaremos os templates (*.html), os endpoints e as funções de visualização como parte do código para o app. Com esses esses três elementos conseguimos implementar a lógica de negócio que desejarmos no app.

Os usuários vão chamar/rodar as funcionalidades do app através da inserção de textos num elemento html chamado TextAreae de cliques em um botão html (<button>).

A parte visual do app será feita com a extensão do pacote Bootstrap para o Flask (Flask-Bootstrap).

Nota: É usual, ao invés de usarmos diretamente uma ferramenta como por ex., o Bootstrap, usarmos sua preferir sua extensão para o Flask (quando houver a extensão). Isso ajuda a integrar Flask e outras ferramentas.

3.1. Script principal (app.py)

Agora devemos criar o seguinte arquivo Python, chamado app.py, na raiz da pasta nome-do-app. É possível criar o arquivo manualmente, através de cliques, ou, se tivermos no Linux, pelo terminal com:

touch app.py
Enter fullscreen mode Exit fullscreen mode

Esse é o código de app.py:

# app.py

## 1. carrega bibliotecas
from flask import Flask, render_template, url_for, session, redirect, flash
from flask_bootstrap import Bootstrap
from flask_moment import Moment

from flask_wtf import FlaskForm
from wtforms import SubmitField, TextAreaField
from wtforms.validators import InputRequired, Length

import os
from datetime import datetime
from utils.constants import *
import random

app = Flask(__name__)
app.config['SECRET_KEY'] = '12345'

bootstrap = Bootstrap(app)
moment = Moment(app)
N_RANDOM = random.randint(7, 300)
##

## 2. funções do modelo de ml
def preprocess_text(some_text: str):
    some_text_processed = ' '.join([word for word in some_text.split(' ') if len(word)])
    return some_text_processed

def run_model(original_text, suspect_text):
    import json
    import requests

    API_URL = os.environ.get('API_URL')
    API_TOKEN = os.environ.get('API_TOKEN')
    headers = {'Authorization': f'Bearer {API_TOKEN}'}
    payload = {'inputs': {'source_sentence': original_text, 'sentences': [suspect_text]}}

    try:
        response = requests.post(API_URL, headers=headers, json=payload)
        res = response.json()
    except Exception as e:
        res = [-1*random.randint(0, 100)/100] # para testes
        print(f'\nErro:\n{e}\n')

    return res

def check_plagiarism(original_text: str, suspect_text: str):
    is_fake = False    

    original_text_clean = preprocess_text(original_text)
    suspect_text_clean = preprocess_text(suspect_text)

    plagiarism_res = run_model(original_text_clean, suspect_text_clean)

    N_MAX_RETRY = 3
    n_retry = 1    
    if type(plagiarism_res) == dict:
        while 'error' in plagiarism_res.keys():
            import time
            try:
              time_waiting = 0.7*plagiarism_res["estimated_time"]
            except:
              time_waiting = 3
            time.sleep(time_waiting)
            plagiarism_res = run_model(original_text_clean, suspect_text_clean)
            n_retry += 1
            if n_retry >= N_MAX_RETRY:
                plagiarism_probab = -1*random.randint(0, 100)/100 # para testes
                break
            if type(plagiarism_res) != dict:
                plagiarism_probab = plagiarism_res[0]
                break
    else:
        plagiarism_probab = plagiarism_res[0]

    return round(plagiarism_probab, 2)
##

## 3. construtor para o objeto formulário
class NameForm(FlaskForm):
    original_text = TextAreaField('Texto Original',
        validators=[InputRequired(), Length(7, 1000)],
        render_kw={'class': 'form-control', 'rows':5,
        'placeholder': '\nEntre com um texto aqui. Por ex.:\n' + LOREM_IPSUM[:N_RANDOM] + ' ...'})

    suspect_text = TextAreaField('Texto Suspeito de Plágio',
        validators=[InputRequired(), Length(7, 1000)],
        render_kw={'class': 'form-control', 'rows':4,
        'placeholder': '\nEntre com outro texto aqui.'})

    submit = SubmitField('Checar')
##

## 4. endpoints e funções de visualização
@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    is_fraud = -1

    session['original_text'] = form.original_text.data
    session['suspect_text'] = form.suspect_text.data

    if form.validate_on_submit():
        original_text = session.get('original_text')
        suspect_text = session.get('suspect_text')
        plagiarism_probab = check_plagiarism(original_text, suspect_text)

        # para testes
        emph = ""
        if plagiarism_probab < 0:
            plagiarism_probab *= -1
            emph = '"'
        # checa probab
        if plagiarism_probab > 0.5:
            is_fraud = 1
            print('Parece plágio!')
        elif plagiarism_probab >= 0:
            is_fraud = 0
            print('Não parece plágio!')
        flash(f'\nTexto {emph}checado{emph}! {emph}Probabilidade de plágio{emph} = {plagiarism_probab*100:.2f} %.\n')        

        return render_template('index.html', form=form,
            user_name=session.get('original_text'), current_time=datetime.utcnow(),
            is_fraud=is_fraud)

    return render_template('index.html', form=form,
        user_name=session.get('original_text'), current_time=datetime.utcnow(),
        is_fraud=is_fraud)

@app.route('/how_to')
def how_to():
    return render_template('how_to.html', current_time=datetime.utcnow())
##

if __name__ == '__main__':
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

O script app.py, mostrado acima, tem 4 partes principais:

  • 1) carrega bibliotecas;
  • 2) funções do modelo de ML;
  • 3) construtor para o objeto formulário;
  • 4) endpoints/rotas e funções de visualização.

A parte 1 carrega as bibliotecas Python que usaremos. Como por ex., as extensões que facilitam a implementação de recursos do Bootstrap (from flask_bootstrap import Bootstrap) e a manipulação segura de formulários (from flask_wtf import FlaskForm).

A parte 2 é onde embutimos o modelo de ML/IA no app. Adiante, falaremos um pouco mais desse modelo.

Já na parte 3 configuramos os formulários do app.
Há benefícios/facilidades quando os formulários podem representados por objetos Python (como aqui) ao invés de se implementar apenas com html. Por ex., fica mais prático manipular os atributos de um formulário e incluir camadas de seguranças.

Por fim, na parte 4 definimos os endpoints (/, /how_to) e suas respectivas funções de visualização (index()e how_to()).

3.2. Separando as utilidades

Como usamos a chamada from utils.constants import * para deixar o código minimamente organizado, devemos criar a pasta utils e dentro desta o arquivo constants.py:

# constants.py

LOREM_IPSUM = '''
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Elementum nibh tellus molestie nunc non blandit massa enim nec. Egestas maecenas pharetra convallis posuere morbi leo urna molestie.
'''
Enter fullscreen mode Exit fullscreen mode

Caso esteja usando um terminal Linux, podemos usar os seguintes comandos:

mkdir utils
touch utils/constants.py
Enter fullscreen mode Exit fullscreen mode

Estrutura atualizada dos arquivos do projeto:

nome-do-app/
│
├── utils/
│   └── constants.py
│
├── venv_app/
│   
├── requirements.txt
└── app.py
Enter fullscreen mode Exit fullscreen mode

3.2. Templates (*.html)

Usaremos três templates para a construção das telas do app: base.html; index.html e how_to.html.

Como o próprio nome diz, o base.html servirá de base para os outros templates html. Nesse arquivo usamos recursos do Bootstrap para que a navegação no app seja a melhor possível.

Já os templates index() e how_to(), são aqueles chamados diretamente pelo app.py. O primeiro organiza as chamadas da tela principal (Home), enquanto o segundo exibe uma imagem animada explicando como usar o app.

Nota: Embora não haja muitos segredos, a definição de plágio pode variar, se é que existe uma única definição. Talvez, variáveis como todos os textos do mundo, influência política, legislação do país e compromisso científico/"vergonha na cara" possam ser levadas em conta na modelagem.

🤖 No entanto, priorizamos pela simplicidade. Portanto, a funcionalidade principal do app é que ele possa realizar a comparação automática (com IA) entre um texto original e um outro suspeito de plágio deste original.

📄 Poderemos incluir melhorias e correções depois que a primeira versão do app estiver em produção. Então, sem mais delongas, na raiz do projeto criemos uma pasta chamada templates.

mkdir templates
Enter fullscreen mode Exit fullscreen mode

A seguir, devemos criar os arquivos base.html, index.html e how_to.html dentro da pasta templates. No Linux, podemos novamente usar o comando touch:

touch templates/base.html templates/index.html templates/how_to.html
Enter fullscreen mode Exit fullscreen mode

Abaixo temos os conteúdos dos arquivos *.html. Note que, como usamos o Flask, esses contêm códigos de Jinja. Essa engine/"linguagem" permite a manipulação de variáveis, listas, objetos, etc. misturando Python e html.

<!-- base.html -->

{% extends 'bootstrap/base.html' %}

{% block title%}{% endblock %}

{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
{% endblock %}

{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{{ moment.locale('pt') }}
{% endblock %}

{% block navbar %}
<ul class="nav nav-pills col-md-3 col-md-offset-8" style="margin-top:5px">
  <li role="presentation" class="active"><a href="{{ url_for('index') }}">Home</a></li>
  <li role="presentation" ><a href="{{ url_for('how_to') }}">Como Usar</a></li>
  <li role="presentation" class="disabled"><a href="#">Sobre</a></li>
</ul>
{% endblock %}

{% block content %}
<div class="container">
    <div class="page-header">
        <h3>Entre com os textos a serem comparados:</h3>
        {{ wtf.quick_form(form) }}
    </div>    
</div>

<div class="container col-md-6 col-md-offset-3">
    {% if is_fraud != -1%}
        {% if is_fraud == 1 %}
        <p><center><b>Parece plágio!</b></center></p>
        <link rel="shortcut icon" href="{{ url_for('static', filename='no.ico') }}" type="image/x-icon">
        <link rel="icon" href="{{ url_for('static', filename='no.ico') }}" type="image/x-icon">
        {% elif is_fraud == 0 %}             
            <p><center><b>Não parece plágio!</b></center></p>
            <link rel="shortcut icon" href="{{ url_for('static', filename='yes.ico') }}" type="image/x-icon">
            <link rel="icon" href="{{ url_for('static', filename='yes.ico') }}" type="image/x-icon">
        {% endif %}
    {% endif %}
</div>
</div>

{% for message in get_flashed_messages() %}
    <div class="alert alert-warning col-md-6 col-md-offset-3">
        <button type="button" class="close" data-dismiss="alert">&times;</button>
        {{ message }}
    </div>
{% endfor %}
{% endblock %}

<hr>
<br>

{%  block body %}
{{ super() }}
<div class="container col-md-6 col-md-offset-3">
    <div>
        <small>
            <p><center>{{ moment(current_time).format('LL') }}</center></p>
        </small>
    </div>
</div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode
<!-- index.html -->

{% extends 'base.html' %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title%}Checador de Plágio Simples - versão β{% endblock %}

{% block flash_message %}{% endblock %}
Enter fullscreen mode Exit fullscreen mode
<!-- how_to.html -->

{% extends 'bootstrap/base.html' %}

{% block title%}Como Usar o Checador de Plágio Simples - versão β{% endblock %}

{% block navbar %}
<ul class="nav nav-pills col-md-3 col-md-offset-8" style="margin-top:5px">
    <li role="presentation"><a href="{{ url_for('index') }}">Home</a></li>
    <li role="presentation" class="active"><a href="{{ url_for('how_to') }}">Como Usar</a></li>
    <li role="presentation" class="disabled"><a href="#">Sobre</a></li>
</ul>
{% endblock %}

{% block content %}
<div class="container">
    <div class="container col-md-9 col-md-offset-1">
    <p style="margin-top:7px"><h4>Siga os passos abaixo para usar este app.</h4><p>
        <div class="thumbnail">

            <img src="{{ url_for('static', filename='how_to.gif') }}">
            {# <img src="{{ url_for('static', filename='how_to.gif') }}" style="max-width:40%; height:auto;"> #}
        </div>
    </div>    
</div>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Após todas as etapas que fizemos até o momento, ficamos com a seguinte estrutura de arquivos na pasta raiz nome-do-app:

nome-do-app/
│
├── templates/
│   ├── base.html
│   ├── index.html
│   └── how_to.html
│
├── utils/
│   └── constants.py
│
├── venv_app/
│
├── requirements.txt
└── app.py
Enter fullscreen mode Exit fullscreen mode

3.3. Inserindo imagens

Nos templates *.html, foi usado uma imagem (*.gif) e ícones (*.ico) que aparecem na barra de título do html. Para usar esses arquivos, criamos uma pasta static na raiz do projeto (nome-do-app) e colocamos os mesmos dentro dessa pasta.

Download das imagens e ícones: Baixe as imagens do link abaixo ou use algumas de sua preferência: link para baixar imagens e ícones.

nome-do-app/
│
├── static
│   ├── favicon.ico
│   ├── how_to.gif
│   ├── no.ico
│   └── yes.ico
│
├── templates/
│   ├── base.html
│   ├── index.html
│   └── how_to.html
│
├── utils/
│   └── constants.py
│
├── venv_app/
│
├── requirements.txt
└── app.py
Enter fullscreen mode Exit fullscreen mode

3.4. Rodar o app localmente

Agora o app está quase pronto. Para rodar a versão atual do app, digite o seguinte comando no terminal:

python app.py
Enter fullscreen mode Exit fullscreen mode

Na sequência, se tudo der certo, vc poderá ver a tela principal do app no endereço e porta padrões do Flask: http://127.0.0.1:5000.

Home - APP

4. Configurando a API de ML ⌨️

No arquivo app.py há constantes que foram usadas para acessarmos a API da Hugging Face, responsável pela checagem do nível de semelhança entre os dois textos de entrada. Se os textos forem muito semelhantes, há uma alta probabilidade de plágio.

Nota: No site da Hugging Face há inúmeros modelos de ML, casos de uso e datasets que podem servir como base para novos/futuros projetos. Por ex., atualmente, na área de Processamento de Linguagem Natural (NLP, em inglês), eles disponibilizam modelos para as mais diferentes tarefas na área.

Hugging Face

4.1. Criando uma conta gratuita na Hugging Face

Lembrando, o modelo de aprendizagem de máquina escolhido para nosso app deverá ter habilidades de verificar a semelhança entre dois textos. A Hugging Face disponibiliza um modelo que pode ajudar nessa tarefa, conhecido como Sentence Similarity.

Como usar um modelo da Hugging Face?

Para usarmos um modelo de Sentence Similarity da Hugging Face via API precisamos ter um token de acesso. Após criarmos uma conta (gratuita) no site da Hugging Face, teremos acesso ao token.

token - hugging face

Esse token é usado para se fazer requisições a API, conforme descrito no site deles.

...

API_URL = 'https://api-inference.huggingface.co/models/sentence-transformers/all-MiniLM-L6-v2'
headers = {'Authorization': f'Bearer {API_TOKEN}'}

...
Enter fullscreen mode Exit fullscreen mode

Com o token e a URL de acesso ao modelo em mãos já podemos usar a API da Hugging Face.

🤔 Se incluirmos de forma explícita (hardcoded) os valores das constantes API_URL e API_TOKEN em nosso programa principal app.py, já conseguiremos realizar as avaliações da suspeita de plágio usando IA via API!

Nota: Mais detalhes sobre o modelo aqui escolhido podem ser encontrados em: https://huggingface.co/tasks/sentence-similarity.

4.2. Definindo as variáveis ambientes (.env)

Para inserir os valores de API_URL e API_TOKEN no arquivo app.py seguindo as boas práticas iremos criar um arquivo chamado .env na raiz do projeto.

Ressaltamos que para cada usuário, teremos um token diferente. O token é um valor secreto e não é recomendável que ele faça parte dos códigos. Além do token, há valores que não são recomendados fazerem parte do código, mas serem chamados de forma implícita.

Como colocaremos o app desenvolvido em produção, seguiremos as recomendações de boas práticas sempre que possível. O arquivo .env deverá ser criado na raiz do projeto e ter a seguinte forma:

# .env

SECRET_KEY_FLASK='!@$%&<mudar-para-uma-secret-key>'
API_URL='https://api-inference.huggingface.co/models/sentence-transformers/all-MiniLM-L6-v2'
API_TOKEN='123456789!@$%&<mudar-para-seu-token>'
Enter fullscreen mode Exit fullscreen mode

Como agora definimos o valor de SECRET_KEY no arquivo .env, devemos retirá-lo do app.py. Para isso, alteramos a seguinte linha neste arquivo:

  • De:
app.config['SECRET_KEY'] = '12345'
Enter fullscreen mode Exit fullscreen mode
  • Para:
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY_FLASK')
Enter fullscreen mode Exit fullscreen mode

Atenção: O valor de API_TOKEN deve ser alterado para o valor da sua conta na Hugging Face conforme mostramos anteriormente.

Após criar o arquivo .env devemos incluir os seguintes comandos em algum lugar no início do arquivo app.py, de forma que os valores de SECRET_KEY, API_URL e API_TOKEN agora fique disponível no app.py, não sendo mais um valor nulo.

# app.py

...

from dotenv import load_dotenv, find_dotenv

load_dotenv(find_dotenv())

...

Enter fullscreen mode Exit fullscreen mode

Os 3 pontinhos no trecho de código acima indicam que o restante do código permanece sem mudanças.

4.3. Preparando para o deploy

Antes de enviar para deploy em produção, devemos verificar se está tudo ok. Para isso rodamos o app desenvolvido localmente (python app.py) e acessamos o localhost (http://127.0.0.1:5000) para navegar nele.

Boas práticas (novamente):

A fim de seguirmos as boas práticas para por um app em produção, ao invés de rodarmos o app chamando diretamente com python app.py, como comumemente é feito na etapa de desenvolvimento, usaremos o WSGI (Web Server Gateway Interface) waitresss como mediador da comunicação entre do servidor e o app em Flask. Com isso o app vai para produção de acordo com as recomendações.

Note que, o waitress já está presente no requirements.txt. Com o waitress ao invés de chamarmos diretamente o app.py para rodar nossa aplicação, iremos chamar o seguinte arquivo main.py, que deve ser criado na raiz do projeto:

# main.py

from waitress import serve

from app import *

if __name__ == '__main__':    
    local_host, local_port = '0.0.0.0', 8080
    print(f'\nServindo em {local_host}:{local_port}')
    serve(app, host=local_host, port=local_port)
Enter fullscreen mode Exit fullscreen mode

Note que no script acima, definimos uma nova porta e endereço de acesso.

Quando um usuário acessar o app pelo navegador a url http://0.0.0.0:8080, ele estará se comunicando com o WSGI (Web Server Gateway Interface,waitress) que, por sua vez, se comunica com o servidor.

Estrutura de arquivos do projeto, após a inclusão do .env e do main.py:

nome-do-app/
│
├── .env
│
├── static
│   ├── favicon.ico
│   ├── how_to.gif
│   ├── no.ico
│   └── yes.ico
│
├── templates/
│   ├── base.html
│   ├── index.html
│   └── how_to.html
│
├── utils/
│   └── constants.py
│
├── venv_app/
│
├── requirements.txt
├── app.py
└── main.py
Enter fullscreen mode Exit fullscreen mode

Agora executamos o seguinte comando para rodar o app:

python main.py
Enter fullscreen mode Exit fullscreen mode

APP - local

Pronto! Basta abrir o app pelo navegador: 0.0.0.0:8080!

Este post foi a PARTE 1 do tutorial composto por duas partes. Aqui vimos como desenvolver um app que utiliza recursos de IA para a tarefa de avaliar uma suspeita de plágio entre dois textos, rodando o app localmente, em em um computador pessoal. Na PARTE 2 veremos como disponibilizar o app na Internet.

Top comments (0)