├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── buildspec.yml ├── cloudformation ├── architecture.png ├── architecture.xml ├── base │ ├── fargate-cluster.yaml │ ├── fargate-service.yaml │ └── vpc-networking.yaml ├── cicd │ └── codebuild.yaml ├── deployment-params.json └── deployment.yaml ├── cloudformation_deploy.sh ├── container_push.sh ├── deployment ├── GPT2 │ ├── config.py │ ├── encoder.json │ ├── encoder.py │ ├── model.py │ ├── sample.py │ ├── utils.py │ └── vocab.bpe ├── run_server.py ├── static │ ├── css │ │ ├── bootstrap.css │ │ ├── bootstrap.min.css │ │ └── main.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── images │ │ └── background.jpg │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── custom.js │ │ ├── ie10-viewport-bug-workaround.js │ │ ├── jquery-1.11.3.min.js │ │ └── jquery.easing.min.js ├── templates │ ├── index.html │ └── seeded.html └── utils.py ├── docker-compose.yml ├── environment.yml └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | LICENCE 2 | README.md 3 | .gitignore 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | 7 | *.vscode/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | 109 | # Idea 110 | .idea/ 111 | 112 | # Misc 113 | models/ 114 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 4 | 5 | COPY ["environment.yml", "/root/environment.yml"] 6 | 7 | RUN apt-get update --fix-missing && apt-get install -y wget bzip2 ca-certificates \ 8 | libglib2.0-0 libxext6 libsm6 libxrender1 \ 9 | git mercurial subversion python-dev gcc 10 | 11 | # install miniconda and python 3.7 12 | RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-4.5.11-Linux-x86_64.sh -O ~/miniconda.sh && \ 13 | /bin/bash ~/miniconda.sh -b -p /opt/conda && \ 14 | rm ~/miniconda.sh && \ 15 | ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \ 16 | echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc 17 | 18 | RUN /opt/conda/bin/conda env create -f=/root/environment.yml -n ml-flask 19 | RUN echo "conda activate ml-flask" >> ~/.bashrc 20 | SHELL ["/bin/bash", "-c", "source ~/.bashrc"] 21 | RUN conda activate ml-flask 22 | 23 | COPY ["deployment", "/usr/src/app/deployment"] 24 | COPY ["models", "/usr/src/app/models"] 25 | 26 | WORKDIR /usr/src/app/deployment 27 | CMD [ "/bin/bash" ] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Will Koehrsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPT-2 Flask API 2 | 3 | Containerising PyTorch models in a repeatable way. Deploy OpenAI's GPT-2 model and expose it over a Flask API. Finally deploy it to AWS Fargate container hosting using CloudFormation. 4 | 5 | ![architecture](cloudformation/architecture.png) 6 | 7 | First, before anything else download the model 8 | 9 | ```bash 10 | mkdir models 11 | curl --output models/gpt2-pytorch_model.bin https://s3.amazonaws.com/models.huggingface.co/bert/gpt2-pytorch_model.bin 12 | ``` 13 | 14 | --- 15 | 16 | ## Local 17 | 18 | --- 19 | 20 | Run the following to get started with your local python environment 21 | 22 | ```bash 23 | python3 -m venv ./venv 24 | source venv/bin/activate 25 | pip install --upgrade pip 26 | pip install -r requirements.txt 27 | ``` 28 | 29 | Then run the python flask server using the following 30 | 31 | ```bash 32 | cd deployment 33 | python run_server.py 34 | ``` 35 | 36 | ### docker-compose 37 | 38 | #### Setup 39 | 40 | ```bash 41 | docker-compose up --build flask 42 | ``` 43 | 44 | Go to [http://localhost:5000](http://localhost:5000) 45 | 46 | #### Shutdown 47 | 48 | ```bash 49 | docker-compose down -v 50 | ``` 51 | 52 | --- 53 | 54 | ## AWS 55 | 56 | --- 57 | 58 | First build and push the container to ECR 59 | 60 | ```bash 61 | ./container_push.sh 62 | ``` 63 | 64 | Setup the CloudFormation stack 65 | 66 | ```bash 67 | ./cloudformation_deploy.sh 68 | ``` 69 | 70 | Deploy the stack 71 | 72 | ```bash 73 | aws cloudformation create-stack \ 74 | --stack-name "gpt-2-flask" \ 75 | --template-body file://cloudformation/deployment.yaml \ 76 | --parameters file://cloudformation/deployment-params.json \ 77 | --capabilities CAPABILITY_IAM 78 | ``` 79 | 80 | --- 81 | 82 | ## Attribution 83 | 84 | --- 85 | 86 | - [WillKoehrsen/recurrent-neural-networks](https://github.com/WillKoehrsen/recurrent-neural-networks) 87 | - [graykode/gpt-2-Pytorch](https://github.com/graykode/gpt-2-Pytorch) 88 | - [Deploying a Keras Deep Learning Model as a Web Application in Python](https://morioh.com/p/bbbc75c00f96/deploying-a-keras-deep-learning-model-as-a-web-application-in-python) 89 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | pre_build: 5 | commands: 6 | - echo Logging in to Amazon ECR... 7 | - dockerd-entrypoint.sh 8 | - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION) 9 | - echo Downloading pytorch model 10 | - mkdir models 11 | - curl --output models/gpt2-pytorch_model.bin https://s3.amazonaws.com/models.huggingface.co/bert/gpt2-pytorch_model.bin 12 | build: 13 | commands: 14 | - echo Build started on `date` 15 | - echo Building the Docker image... 16 | - docker build -t $IMAGE_REPO_NAME . 17 | - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG 18 | post_build: 19 | commands: 20 | - echo Build completed on `date` 21 | - echo Pushing the Docker image... 22 | - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG 23 | - aws ecs update-service --cluster $FARGATE_CLUSTER --service $FARGATE_SERVICE --force-new-deployment 24 | -------------------------------------------------------------------------------- /cloudformation/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t04glovern/gpt-2-flask-api/a4930a5308e6f8aa37ac20995032343131da185e/cloudformation/architecture.png -------------------------------------------------------------------------------- /cloudformation/architecture.xml: -------------------------------------------------------------------------------- 1 | 7VvbTus4FP2aPlIlcW59hALn6IiR0DCamfOE3MSkBjeOHAfofP3YjXN32xQSKKIgoXjbcZy91r7ZYQLmq9cfDCbLP2iIyMQywtcJuJxYluf44q8UrHOBabhKEjEcKlkluMP/oWKgkmY4RGljIKeUcJw0hQGNYxTwhgwyRl+awx4oaT41gRHqCO4CSLrSf3DIl0pqGkbV8RPhaKke7TuqYwGDp4jRLFbPm1jgYfOTd69gMZcany5hSF9qInA1AXNGKc+vVq9zRKRuC7Xl911v6S3XzVDM+9zg/OKP88fHGLtX2R3Nfibkr/jMzWd5hiRDxWtsFsvXhYI2r4jkJOYEXEAWKAxt0Qphutx0GaKRckaf0JwSyjZ3AmPzI3oeMCGFPKaxuPsiYjDEYuUtcT5HAYQlJYXa5EQBXeFAXT/QmF/DFSaSeX8jFsIYKrFaoWmptm5NS74i6p26mlTKfUaMo9eaSGn2B6IrxNlaDFG9ju3ltygzAJ7S7EvFKSHNZcsancyCTlDxOCrnrrAUFwpOPbRuaHqONwuNmTezTcs/s0+wDgKrOTMasBbNGqqub3RRtf0BUNUarHVCdhBky/hTxC1TA62tg9YeC9qT0Q4DLTCtvb64NOzBfbEWWnCCdhRoy+RqL7T2bCyHPJTZmt8cW9t0mtiCLra6WFsG5UEzKGc/qg2rrOlCqDuRQ1avkSyZpvAlBdOA0Cxs4Sh0ee34DnC3g9l4RpNGW1HtkOjKlb8darjvZ8AIKZbbRd2xuqgDdyR7NjXAu0Q89WIhLiJ5YRpT0zWm4lc8wXSLftEoh3TIQjNOcCxQKapYo42uVBkWdekNXCByS1PMMY1F34JyTle1AecER7KD00R6E9UKBASI9WDicxIczsMBgPa8Vi7djcq6fGu0mNzDwk8x+Q2lr7DoDrDjlb5aaL39NnybLYjQp2WcfzHzFZzk6J7DhVzaJ5gxAK0UzOg6bGBrHLY9Eth+D7AZfoYcndA+3Lhd77jQnh2C9sV3Qpu0Flc+dQQaAKfr4z+UBsVuTS8X/61oMIKL/3y0h9r1/O41tmu13LnVRbY8+WoU2f5o+ydmB0oURuhONSnjSxrRGJKrSlqrhFs6Eqph63+VfNP4LRtTyynal6/13su1aj0iztcKEJhxKkTVo2+oNOTdIKQ0YwHaRWKVAnPIIsR3DVTklmrYCSpDBHL8jBrrGN74dJuXLVd7fvPVfCxMEhEcoJz9nlAY3i8ggXGwuVfnce2PTap9s+tx3a5ZjnYWoXGwI1ildwRWaX9Rq9RtO5+scmCrbO5I21Z3y+pDrdLU7Vm1QL8WRM6rnznJUqn2cSmQI93GvyTGwRRAQXo42GPWPp4xa/pmw+vFgtGyYTCob+7rYlPhIfm5/OBK6pfANJUpbi6+xqRy9WExSGEjJKp/54nBfk896+mpgXlcnrrHloWwOw6F/bH0izlsHAsCCAf9Bpsdopxx7Gnr0NADhaR+ruB8ZOrUY2vihPdAeNum/8lo96iO5jREFxkm4XGEYparZf+RtVj2YrPsDtaeM5tLJfWNxwQ98GEoYHte2+QN0I3HGgoAfywKzIaMx1WtNC2ro9+TenGkL5U2rVvEsHgpacofVD9ZTs+oXBjKcFF5c6tINuC6NiChOOZpbeZbKaj4Y82aO2Cm1/pqujXenjm7xouLfAUVfcpXeQejunn+knP5Qfy5fLJ1HWG+zBbCQleiwQ07IlSYUCx7En5mnT2I/OzpDCa4w8yGI9G6mhoXC4+hzLdt1YUj2eaPVjgMN2Rvu6Syo+13XtCCCHKm0/wFB/AXltfC23J1CYJmyxOMteNZ5KeakBHi5youbBWVseNq/qc+pJQyzd1t6WDhZz/cLT7tLwXfUPdvI+kg6Yffij16NgHvI9nkDRl9GlVeedhRlXhGs8Sr6sCqyjukqHx73AF9d9PtocPO+9DyT2jtrN2PC60e320fT20WZKmYD7F7uf/3InOSTyjRyi+9irNj29PFW83psQ1GQtHWFeRHi6IgdirmucfJp+Bntr7SBK52Q0Wz32keXmKLZvX/j3nyXP2TKbj6Hw== -------------------------------------------------------------------------------- /cloudformation/base/fargate-cluster.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Deploys a Fargate cluster 3 | 4 | Parameters: 5 | 6 | ProjectName: 7 | Description: Project Name 8 | Type: String 9 | ClusterName: 10 | Description: Fargate Cluster Name 11 | Type: String 12 | 13 | Resources: 14 | 15 | FargateCluster: 16 | Type: 'AWS::ECS::Cluster' 17 | Properties: 18 | ClusterName: !Ref ClusterName 19 | 20 | Outputs: 21 | 22 | FargateCluster: 23 | Description: Fargate Cluster 24 | Value: !Ref FargateCluster 25 | -------------------------------------------------------------------------------- /cloudformation/base/fargate-service.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Deploy a service on AWS Fargate 3 | 4 | Parameters: 5 | 6 | ServiceName: 7 | Type: String 8 | Description: A name for the service 9 | 10 | VPCId: 11 | Type: 'AWS::EC2::VPC::Id' 12 | Description: VPC that should house this cluster 13 | PublicSubnetIDs: 14 | Type: 'List' 15 | Description: Subnet IDs for the ALB 16 | PrivateSubnetIDs: 17 | Type: 'List' 18 | Description: Subnet IDs for the Fargate Containers 19 | FargateCluster: 20 | Type: String 21 | Description: Fargate Cluster for this service to run on 22 | 23 | ContainerName: 24 | Type: String 25 | Description: The name of a docker image that should be run from ECR 26 | ContainerPort: 27 | Type: Number 28 | Description: What port number the application inside the docker container 29 | ContainerCpu: 30 | Type: Number 31 | Description: How much CPU to give the container. 1024 is 1 CPU 32 | ContainerMemory: 33 | Type: Number 34 | Description: How much memory in megabytes to give the container 35 | 36 | DesiredCount: 37 | Type: Number 38 | Description: How many copies of the service task to run 39 | HealthEndpoint: 40 | Type: String 41 | Description: Health Endpoint to target via Application load balancer health checks 42 | 43 | Resources: 44 | 45 | fargateTaskExecutionRole: 46 | Type: 'AWS::IAM::Role' 47 | Properties: 48 | AssumeRolePolicyDocument: 49 | Statement: 50 | - Effect: Allow 51 | Principal: 52 | Service: 53 | - ecs-tasks.amazonaws.com 54 | Action: 55 | - 'sts:AssumeRole' 56 | Path: / 57 | Policies: 58 | - PolicyName: logs 59 | PolicyDocument: 60 | Statement: 61 | - Effect: Allow 62 | Action: 63 | - 'logs:CreateLogGroup' 64 | - 'logs:CreateLogStream' 65 | - 'logs:PutLogEvents' 66 | Resource: '*' 67 | - PolicyName: ecr 68 | PolicyDocument: 69 | Statement: 70 | - Effect: Allow 71 | Action: 72 | - 'ecr:BatchCheckLayerAvailability' 73 | - 'ecr:GetDownloadUrlForLayer' 74 | - 'ecr:BatchGetImage' 75 | Resource: !Join 76 | - '' 77 | - - 'arn:aws:ecr:' 78 | - !Ref 'AWS::Region' 79 | - ':' 80 | - !Ref 'AWS::AccountId' 81 | - ':repository/' 82 | - !Ref ContainerName 83 | - Effect: Allow 84 | Action: 85 | - 'ecr:GetAuthorizationToken' 86 | Resource: '*' 87 | 88 | fargateLogGroup: 89 | Type: AWS::Logs::LogGroup 90 | Properties: 91 | LogGroupName: !Sub /fargate/${AWS::StackName} 92 | RetentionInDays: 7 93 | 94 | fargateTaskDefinition: 95 | Type: 'AWS::ECS::TaskDefinition' 96 | Properties: 97 | Family: !Sub '${ServiceName}-task' 98 | Cpu: !Ref ContainerCpu 99 | Memory: !Ref ContainerMemory 100 | NetworkMode: awsvpc 101 | RequiresCompatibilities: 102 | - FARGATE 103 | ExecutionRoleArn: !Ref fargateTaskExecutionRole 104 | ContainerDefinitions: 105 | - Name: !Sub '${ServiceName}' 106 | Cpu: !Ref ContainerCpu 107 | Memory: !Ref ContainerMemory 108 | Image: !Join 109 | - '' 110 | - - !Ref 'AWS::AccountId' 111 | - '.dkr.ecr.' 112 | - !Ref 'AWS::Region' 113 | - '.amazonaws.com/' 114 | - !Ref ContainerName 115 | - ':latest' 116 | PortMappings: 117 | - ContainerPort: !Ref ContainerPort 118 | EntryPoint: 119 | - /opt/conda/envs/ml-flask/bin/python 120 | - run_server.py 121 | LogConfiguration: 122 | LogDriver: awslogs 123 | Options: 124 | awslogs-region: !Ref AWS::Region 125 | awslogs-group: !Ref fargateLogGroup 126 | awslogs-stream-prefix: !Ref AWS::StackName 127 | 128 | fargateContainerSecurityGroup: 129 | Type: 'AWS::EC2::SecurityGroup' 130 | Properties: 131 | GroupDescription: Access to the Fargate containers 132 | SecurityGroupIngress: 133 | - Description: Container port 134 | IpProtocol: tcp 135 | FromPort: !Ref ContainerPort 136 | ToPort: !Ref ContainerPort 137 | SourceSecurityGroupId: !Ref loadBalancerSecurityGroup 138 | VpcId: !Ref VPCId 139 | 140 | fargateService: 141 | Type: 'AWS::ECS::Service' 142 | DependsOn: 143 | - httpLoadBalancerListener 144 | Properties: 145 | ServiceName: !Sub '${ServiceName}-service' 146 | Cluster: !Ref FargateCluster 147 | LaunchType: FARGATE 148 | DeploymentConfiguration: 149 | MaximumPercent: 200 150 | MinimumHealthyPercent: 75 151 | DesiredCount: !Ref DesiredCount 152 | HealthCheckGracePeriodSeconds: 60 153 | NetworkConfiguration: 154 | AwsvpcConfiguration: 155 | AssignPublicIp: ENABLED 156 | SecurityGroups: 157 | - !Ref fargateContainerSecurityGroup 158 | Subnets: !Ref PrivateSubnetIDs 159 | TaskDefinition: !Ref fargateTaskDefinition 160 | LoadBalancers: 161 | - ContainerName: !Sub '${ServiceName}' 162 | ContainerPort: !Ref ContainerPort 163 | TargetGroupArn: !Ref targetGroup 164 | 165 | loadBalancerSecurityGroup: 166 | Type: 'AWS::EC2::SecurityGroup' 167 | Properties: 168 | GroupDescription: Access to the frontend loadbalancer 169 | SecurityGroupIngress: 170 | - CidrIp: 0.0.0.0/0 171 | Description: HTTP Web Port from Load Balancer 172 | IpProtocol: tcp 173 | FromPort: 80 174 | ToPort: 80 175 | VpcId: !Ref VPCId 176 | 177 | loadBalancer: 178 | Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' 179 | Properties: 180 | Name: !Join 181 | - '-' 182 | - - !Ref ServiceName 183 | - lb 184 | Scheme: internet-facing 185 | LoadBalancerAttributes: 186 | - Key: idle_timeout.timeout_seconds 187 | Value: '120' 188 | SecurityGroups: 189 | - !Ref loadBalancerSecurityGroup 190 | Subnets: !Ref PublicSubnetIDs 191 | 192 | httpLoadBalancerListener: 193 | Type: 'AWS::ElasticLoadBalancingV2::Listener' 194 | Properties: 195 | DefaultActions: 196 | - TargetGroupArn: !Ref targetGroup 197 | Type: forward 198 | LoadBalancerArn: !Ref loadBalancer 199 | Port: 80 200 | Protocol: HTTP 201 | 202 | targetGroup: 203 | Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' 204 | Properties: 205 | HealthCheckIntervalSeconds: 6 206 | HealthCheckPath: !Ref HealthEndpoint 207 | HealthCheckProtocol: HTTP 208 | HealthCheckTimeoutSeconds: 5 209 | HealthyThresholdCount: 2 210 | TargetType: ip 211 | Name: !Join 212 | - '-' 213 | - - !Ref ServiceName 214 | - fwd 215 | Port: !Ref ContainerPort 216 | Protocol: HTTP 217 | UnhealthyThresholdCount: 2 218 | VpcId: !Ref VPCId 219 | 220 | Outputs: 221 | 222 | LoadBalancerDNSName: 223 | Description: DNS name for the created loadbalancer. 224 | Value: !GetAtt 225 | - loadBalancer 226 | - DNSName 227 | 228 | EndpointUrl: 229 | Description: Request URL for the API endpoint 230 | Value: !Join 231 | - '' 232 | - - !GetAtt 233 | - loadBalancer 234 | - DNSName 235 | - '/' 236 | -------------------------------------------------------------------------------- /cloudformation/base/vpc-networking.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Copyright 2018 widdix GmbH 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | AWSTemplateFormatVersion: 2010-09-09 16 | Description: 'VPC: public and private subnets in two availability zones, a cloudonaut.io template' 17 | 18 | Metadata: 19 | 'AWS::CloudFormation::Interface': 20 | ParameterGroups: 21 | - Label: 22 | default: 'VPC Parameters' 23 | Parameters: 24 | - ClassB 25 | 26 | Parameters: 27 | 28 | ClassB: 29 | Description: 'Class B of VPC (10.XXX.0.0/16)' 30 | Type: Number 31 | Default: 0 32 | ConstraintDescription: 'Must be in the range [0-255]' 33 | MinValue: 0 34 | MaxValue: 255 35 | 36 | Resources: 37 | 38 | VPC: 39 | Type: 'AWS::EC2::VPC' 40 | Properties: 41 | CidrBlock: !Sub '10.${ClassB}.0.0/16' 42 | EnableDnsSupport: true 43 | EnableDnsHostnames: true 44 | InstanceTenancy: default 45 | Tags: 46 | - Key: Name 47 | Value: !Sub '10.${ClassB}.0.0/16' 48 | 49 | InternetGateway: 50 | Type: 'AWS::EC2::InternetGateway' 51 | Properties: 52 | Tags: 53 | - Key: Name 54 | Value: !Sub '10.${ClassB}.0.0/16' 55 | 56 | VPCGatewayAttachment: 57 | Type: 'AWS::EC2::VPCGatewayAttachment' 58 | Properties: 59 | VpcId: !Ref VPC 60 | InternetGatewayId: !Ref InternetGateway 61 | 62 | SubnetAPublic: 63 | Type: 'AWS::EC2::Subnet' 64 | Properties: 65 | AvailabilityZone: !Select [0, !GetAZs ''] 66 | CidrBlock: !Sub '10.${ClassB}.0.0/20' 67 | MapPublicIpOnLaunch: true 68 | VpcId: !Ref VPC 69 | Tags: 70 | - Key: Name 71 | Value: 'A public' 72 | - Key: Reach 73 | Value: public 74 | 75 | SubnetAPrivate: 76 | Type: 'AWS::EC2::Subnet' 77 | Properties: 78 | AvailabilityZone: !Select [0, !GetAZs ''] 79 | CidrBlock: !Sub '10.${ClassB}.16.0/20' 80 | VpcId: !Ref VPC 81 | Tags: 82 | - Key: Name 83 | Value: 'A private' 84 | - Key: Reach 85 | Value: private 86 | 87 | SubnetBPublic: 88 | Type: 'AWS::EC2::Subnet' 89 | Properties: 90 | AvailabilityZone: !Select [1, !GetAZs ''] 91 | CidrBlock: !Sub '10.${ClassB}.32.0/20' 92 | MapPublicIpOnLaunch: true 93 | VpcId: !Ref VPC 94 | Tags: 95 | - Key: Name 96 | Value: 'B public' 97 | - Key: Reach 98 | Value: public 99 | 100 | SubnetBPrivate: 101 | Type: 'AWS::EC2::Subnet' 102 | Properties: 103 | AvailabilityZone: !Select [1, !GetAZs ''] 104 | CidrBlock: !Sub '10.${ClassB}.48.0/20' 105 | VpcId: !Ref VPC 106 | Tags: 107 | - Key: Name 108 | Value: 'B private' 109 | - Key: Reach 110 | Value: private 111 | 112 | NatGatewayAAttachment: 113 | Type: AWS::EC2::EIP 114 | DependsOn: VPCGatewayAttachment 115 | Properties: 116 | Domain: vpc 117 | 118 | NatGatewayBAttachment: 119 | Type: AWS::EC2::EIP 120 | DependsOn: VPCGatewayAttachment 121 | Properties: 122 | Domain: vpc 123 | 124 | NatGatewayA: 125 | Type: AWS::EC2::NatGateway 126 | Properties: 127 | AllocationId: !GetAtt NatGatewayAAttachment.AllocationId 128 | SubnetId: !Ref SubnetAPublic 129 | 130 | NatGatewayB: 131 | Type: AWS::EC2::NatGateway 132 | Properties: 133 | AllocationId: !GetAtt NatGatewayBAttachment.AllocationId 134 | SubnetId: !Ref SubnetBPublic 135 | 136 | RouteTableAPublic: 137 | Type: 'AWS::EC2::RouteTable' 138 | Properties: 139 | VpcId: !Ref VPC 140 | Tags: 141 | - Key: Name 142 | Value: 'A Public' 143 | 144 | RouteTableAPrivate: 145 | Type: 'AWS::EC2::RouteTable' 146 | Properties: 147 | VpcId: !Ref VPC 148 | Tags: 149 | - Key: Name 150 | Value: 'A Private' 151 | 152 | RouteTableBPublic: 153 | Type: 'AWS::EC2::RouteTable' 154 | Properties: 155 | VpcId: !Ref VPC 156 | Tags: 157 | - Key: Name 158 | Value: 'B Public' 159 | 160 | RouteTableBPrivate: 161 | Type: 'AWS::EC2::RouteTable' 162 | Properties: 163 | VpcId: !Ref VPC 164 | Tags: 165 | - Key: Name 166 | Value: 'B Private' 167 | 168 | RouteTableAssociationAPublic: 169 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 170 | Properties: 171 | SubnetId: !Ref SubnetAPublic 172 | RouteTableId: !Ref RouteTableAPublic 173 | 174 | RouteTableAssociationAPrivate: 175 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 176 | Properties: 177 | SubnetId: !Ref SubnetAPrivate 178 | RouteTableId: !Ref RouteTableAPrivate 179 | 180 | RouteTableAssociationBPublic: 181 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 182 | Properties: 183 | SubnetId: !Ref SubnetBPublic 184 | RouteTableId: !Ref RouteTableBPublic 185 | 186 | RouteTableAssociationBPrivate: 187 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 188 | Properties: 189 | SubnetId: !Ref SubnetBPrivate 190 | RouteTableId: !Ref RouteTableBPrivate 191 | 192 | RouteTablePublicAInternetRoute: 193 | Type: 'AWS::EC2::Route' 194 | DependsOn: VPCGatewayAttachment 195 | Properties: 196 | RouteTableId: !Ref RouteTableAPublic 197 | DestinationCidrBlock: '0.0.0.0/0' 198 | GatewayId: !Ref InternetGateway 199 | 200 | RouteTablePublicBInternetRoute: 201 | Type: 'AWS::EC2::Route' 202 | DependsOn: VPCGatewayAttachment 203 | Properties: 204 | RouteTableId: !Ref RouteTableBPublic 205 | DestinationCidrBlock: '0.0.0.0/0' 206 | GatewayId: !Ref InternetGateway 207 | 208 | RouteTablePrivateANatRoute: 209 | Type: AWS::EC2::Route 210 | Properties: 211 | RouteTableId: !Ref RouteTableAPrivate 212 | DestinationCidrBlock: '0.0.0.0/0' 213 | NatGatewayId: !Ref NatGatewayA 214 | 215 | RouteTablePrivateBNatRoute: 216 | Type: AWS::EC2::Route 217 | Properties: 218 | RouteTableId: !Ref RouteTableBPrivate 219 | DestinationCidrBlock: '0.0.0.0/0' 220 | NatGatewayId: !Ref NatGatewayB 221 | 222 | NetworkAclPublic: 223 | Type: 'AWS::EC2::NetworkAcl' 224 | Properties: 225 | VpcId: !Ref VPC 226 | Tags: 227 | - Key: Name 228 | Value: Public 229 | 230 | NetworkAclPrivate: 231 | Type: 'AWS::EC2::NetworkAcl' 232 | Properties: 233 | VpcId: !Ref VPC 234 | Tags: 235 | - Key: Name 236 | Value: Private 237 | 238 | SubnetNetworkAclAssociationAPublic: 239 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 240 | Properties: 241 | SubnetId: !Ref SubnetAPublic 242 | NetworkAclId: !Ref NetworkAclPublic 243 | 244 | SubnetNetworkAclAssociationAPrivate: 245 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 246 | Properties: 247 | SubnetId: !Ref SubnetAPrivate 248 | NetworkAclId: !Ref NetworkAclPrivate 249 | 250 | SubnetNetworkAclAssociationBPublic: 251 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 252 | Properties: 253 | SubnetId: !Ref SubnetBPublic 254 | NetworkAclId: !Ref NetworkAclPublic 255 | 256 | SubnetNetworkAclAssociationBPrivate: 257 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 258 | Properties: 259 | SubnetId: !Ref SubnetBPrivate 260 | NetworkAclId: !Ref NetworkAclPrivate 261 | 262 | NetworkAclEntryInPublicAllowAll: 263 | Type: 'AWS::EC2::NetworkAclEntry' 264 | Properties: 265 | NetworkAclId: !Ref NetworkAclPublic 266 | RuleNumber: 99 267 | Protocol: -1 268 | RuleAction: allow 269 | Egress: false 270 | CidrBlock: '0.0.0.0/0' 271 | 272 | NetworkAclEntryOutPublicAllowAll: 273 | Type: 'AWS::EC2::NetworkAclEntry' 274 | Properties: 275 | NetworkAclId: !Ref NetworkAclPublic 276 | RuleNumber: 99 277 | Protocol: -1 278 | RuleAction: allow 279 | Egress: true 280 | CidrBlock: '0.0.0.0/0' 281 | 282 | NetworkAclEntryInPrivateAllowVPC: 283 | Type: 'AWS::EC2::NetworkAclEntry' 284 | Properties: 285 | NetworkAclId: !Ref NetworkAclPrivate 286 | RuleNumber: 99 287 | Protocol: -1 288 | RuleAction: allow 289 | Egress: false 290 | CidrBlock: '0.0.0.0/0' 291 | 292 | NetworkAclEntryOutPrivateAllowVPC: 293 | Type: 'AWS::EC2::NetworkAclEntry' 294 | Properties: 295 | NetworkAclId: !Ref NetworkAclPrivate 296 | RuleNumber: 99 297 | Protocol: -1 298 | RuleAction: allow 299 | Egress: true 300 | CidrBlock: '0.0.0.0/0' 301 | 302 | Outputs: 303 | 304 | VPC: 305 | Description: 'VPC.' 306 | Value: !Ref VPC 307 | 308 | SubnetsPublic: 309 | Description: 'Subnets public.' 310 | Value: !Join [',', [!Ref SubnetAPublic, !Ref SubnetBPublic]] 311 | 312 | SubnetsPrivate: 313 | Description: 'Subnets private.' 314 | Value: !Join [',', [!Ref SubnetAPrivate, !Ref SubnetBPrivate]] 315 | -------------------------------------------------------------------------------- /cloudformation/cicd/codebuild.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Codebuild Template for building from GitHub Source 3 | 4 | Parameters: 5 | 6 | VPC: 7 | Description: VPC to operate in 8 | Type: 'AWS::EC2::VPC::Id' 9 | SubnetIDs: 10 | Description: Subnet IDs that is a List of Subnet Id 11 | Type: 'List' 12 | 13 | FargateCluster: 14 | Description: Fargate cluster where this task runs 15 | Type: String 16 | FargateService: 17 | Description: Fargate service where the image is run from 18 | Type: String 19 | 20 | CodeRepositoryURL: 21 | Description: URL of the repo which contains CFN template. 22 | Type: String 23 | BuildspecLocation: 24 | Description: Location of buildspec configuration 25 | Type: String 26 | ImageRepoName: 27 | Description: Container Repository name (just the end bit) 28 | Type: String 29 | EnvironmentType: 30 | Description: Type of build environment to use for related builds. 31 | Type: String 32 | AllowedValues: 33 | - WINDOWS_CONTAINER 34 | - LINUX_CONTAINER 35 | ComputeType: 36 | Description: Compute resources the build project will use to build. 37 | Type: String 38 | AllowedValues: 39 | - BUILD_GENERAL1_SMALL 40 | - BUILD_GENERAL1_MEDIUM 41 | - BUILD_GENERAL1_LARGE 42 | BuildImage: 43 | Description: >- 44 | System Image identifier of the image to use for code build 45 | (https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html) 46 | Type: String 47 | 48 | Resources: 49 | 50 | codeBuildSecurityGroup: 51 | Type: 'AWS::EC2::SecurityGroup' 52 | Properties: 53 | GroupDescription: Basic Security group for codebuild 54 | VpcId: !Ref VPC 55 | 56 | codeBuildServiceRole: 57 | Type: 'AWS::IAM::Role' 58 | Properties: 59 | AssumeRolePolicyDocument: 60 | Version: 2012-10-17 61 | Statement: 62 | - Effect: Allow 63 | Principal: 64 | Service: codebuild.amazonaws.com 65 | Action: 'sts:AssumeRole' 66 | Path: / 67 | Policies: 68 | - PolicyName: codebuild 69 | PolicyDocument: 70 | Version: 2012-10-17 71 | Statement: 72 | - Effect: Allow 73 | Action: 74 | - 'logs:CreateLogGroup' 75 | - 'logs:CreateLogStream' 76 | - 'logs:PutLogEvents' 77 | Resource: !Sub >- 78 | arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/* 79 | - Effect: Allow 80 | Action: 81 | - 's3:PutObject' 82 | - 's3:GetObject' 83 | - 's3:GetObjectVersion' 84 | Resource: !Sub 'arn:aws:s3:::codepipeline-${AWS::Region}-*' 85 | - Effect: Allow 86 | Action: 87 | - 'ecr:BatchCheckLayerAvailability' 88 | - 'ecr:CompleteLayerUpload' 89 | - 'ecr:GetAuthorizationToken' 90 | - 'ecr:InitiateLayerUpload' 91 | - 'ecr:PutImage' 92 | - 'ecr:UploadLayerPart' 93 | - 'ecs:UpdateService' 94 | Resource: '*' 95 | - PolicyName: codebuild-vpc 96 | PolicyDocument: 97 | Version: 2012-10-17 98 | Statement: 99 | - Effect: Allow 100 | Action: 101 | - 'ec2:CreateNetworkInterface' 102 | - 'ec2:DescribeDhcpOptions' 103 | - 'ec2:DescribeNetworkInterfaces' 104 | - 'ec2:DeleteNetworkInterface' 105 | - 'ec2:DescribeSubnets' 106 | - 'ec2:DescribeSecurityGroups' 107 | - 'ec2:DescribeVpcs' 108 | Resource: '*' 109 | - Effect: Allow 110 | Action: 111 | - 'ec2:CreateNetworkInterfacePermission' 112 | Condition: 113 | StringEquals: 114 | 'ec2:Subnet': 115 | - !Join 116 | - '' 117 | - - 'arn:aws:ec2:' 118 | - !Ref 'AWS::Region' 119 | - ':' 120 | - !Ref 'AWS::AccountId' 121 | - ':subnet/' 122 | - !Select 123 | - '0' 124 | - !Ref SubnetIDs 125 | - !Join 126 | - '' 127 | - - 'arn:aws:ec2:' 128 | - !Ref 'AWS::Region' 129 | - ':' 130 | - !Ref 'AWS::AccountId' 131 | - ':subnet/' 132 | - !Select 133 | - '1' 134 | - !Ref SubnetIDs 135 | 'ec2:AuthorizedService': codebuild.amazonaws.com 136 | Resource: !Sub >- 137 | arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/* 138 | - Effect: Allow 139 | Action: 140 | - 'ecr:BatchCheckLayerAvailability' 141 | - 'ecr:CompleteLayerUpload' 142 | - 'ecr:GetAuthorizationToken' 143 | - 'ecr:InitiateLayerUpload' 144 | - 'ecr:PutImage' 145 | - 'ecr:UploadLayerPart' 146 | Resource: !Sub >- 147 | arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ImageRepoName} 148 | 149 | codeBuildProject: 150 | Type: 'AWS::CodeBuild::Project' 151 | Properties: 152 | Source: 153 | Location: !Ref CodeRepositoryURL 154 | Type: GITHUB 155 | Auth: 156 | Type: OAUTH 157 | BuildSpec: !Ref BuildspecLocation 158 | Artifacts: 159 | Type: NO_ARTIFACTS 160 | Cache: 161 | Type: NO_CACHE 162 | Environment: 163 | ComputeType: !Ref ComputeType 164 | Image: !Ref BuildImage 165 | Type: !Ref EnvironmentType 166 | EnvironmentVariables: 167 | - Name: AWS_DEFAULT_REGION 168 | Value: !Sub '${AWS::Region}' 169 | - Name: AWS_ACCOUNT_ID 170 | Value: !Sub '${AWS::AccountId}' 171 | - Name: IMAGE_REPO_NAME 172 | Value: !Ref ImageRepoName 173 | - Name: IMAGE_TAG 174 | Value: latest 175 | - Name: FARGATE_CLUSTER 176 | Value: !Ref FargateCluster 177 | - Name: FARGATE_SERVICE 178 | Value: !Ref FargateService 179 | PrivilegedMode: true 180 | VpcConfig: 181 | VpcId: !Ref VPC 182 | Subnets: !Ref SubnetIDs 183 | SecurityGroupIds: 184 | - !Ref codeBuildSecurityGroup 185 | Name: !Sub '${ImageRepoName}-cb' 186 | Description: !Sub 'CodeBuild for ${ImageRepoName}' 187 | EncryptionKey: !Sub 'arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3' 188 | ServiceRole: !Ref codeBuildServiceRole 189 | TimeoutInMinutes: 60 190 | BadgeEnabled: true 191 | -------------------------------------------------------------------------------- /cloudformation/deployment-params.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "ParameterKey":"ProjectName", "ParameterValue":"GPT2 Flask API" }, 3 | { "ParameterKey":"BucketName", "ParameterValue":"devopstar" }, 4 | { "ParameterKey":"ClassB", "ParameterValue":"160" }, 5 | { "ParameterKey":"ClusterName", "ParameterValue":"gpt-2-flask-cluster" }, 6 | { "ParameterKey":"CodeRepositoryURL", "ParameterValue":"https://github.com/t04glovern/gpt-2-flask-api" }, 7 | 8 | { "ParameterKey":"ServiceName", "ParameterValue":"gpt-2-flask-api" }, 9 | { "ParameterKey":"ContainerName", "ParameterValue":"gpt-2-flask-api" }, 10 | { "ParameterKey":"ContainerPort", "ParameterValue":"5000" }, 11 | { "ParameterKey":"ContainerCpu", "ParameterValue":"2048" }, 12 | { "ParameterKey":"ContainerMemory", "ParameterValue":"8192" }, 13 | { "ParameterKey":"DesiredCount", "ParameterValue":"1" }, 14 | { "ParameterKey":"HealthEndpoint", "ParameterValue":"/" }, 15 | 16 | { "ParameterKey":"BuildspecLocation", "ParameterValue":"buildspec.yml" }, 17 | { "ParameterKey":"EnvironmentType", "ParameterValue":"LINUX_CONTAINER" }, 18 | { "ParameterKey":"ComputeType", "ParameterValue":"BUILD_GENERAL1_SMALL" }, 19 | { "ParameterKey":"BuildImage", "ParameterValue":"aws/codebuild/ubuntu-base:14.04" } 20 | ] 21 | -------------------------------------------------------------------------------- /cloudformation/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | 4 | Description: Nested Fargate Stack deployment 5 | 6 | Metadata: 7 | 8 | Authors: 9 | Description: Nathan Glover (nathan@glovers.id.au) 10 | 11 | AWS::CloudFormation::Interface: 12 | ParameterGroups: 13 | - Label: 14 | default: Project Information 15 | Parameters: 16 | - ProjectName 17 | - BucketName 18 | 19 | - Label: 20 | default: Networking Resources 21 | Parameters: 22 | - ClassB 23 | 24 | - Label: 25 | default: Base Fargate Cluster 26 | Parameters: 27 | - ClusterName 28 | 29 | - Label: 30 | default: Fargate Service 31 | Parameters: 32 | - ServiceName 33 | - ContainerName 34 | - ContainerPort 35 | - ContainerCpu 36 | - ContainerMemory 37 | - DesiredCount 38 | - HealthEndpoint 39 | 40 | - Label: 41 | default: Codebuild Base 42 | Parameters: 43 | - CodeRepositoryURL 44 | 45 | - Label: 46 | default: Codebuild Service 47 | Parameters: 48 | - BuildspecLocation 49 | - EnvironmentType 50 | - ComputeType 51 | - BuildImage 52 | 53 | ParameterLabels: 54 | ProjectName: 55 | default: Project Name 56 | BucketName: 57 | default: Bucket Name 58 | 59 | ClassB: 60 | default: (10.XXX.0.0/16) 61 | 62 | ClusterName: 63 | default: Cluster Name 64 | 65 | ServiceName: 66 | default: Service Name 67 | ContainerName: 68 | default: Docker Image Name 69 | ContainerPort: 70 | default: Container Port 71 | ContainerCpu: 72 | default: Container Cpu 73 | ContainerMemory: 74 | default: Container Memory 75 | DesiredCount: 76 | default: Number of Tasks 77 | HealthEndpoint: 78 | default: Health Check Endpoint 79 | 80 | CodeRepositoryURL: 81 | default: Git Repo URL 82 | 83 | BuildspecLocation: 84 | default: buildspec file 85 | EnvironmentType: 86 | default: Build OS 87 | ComputeType: 88 | default: Build Resource 89 | BuildImage: 90 | default: Build Image 91 | 92 | Parameters: 93 | 94 | ProjectName: 95 | Description: Project Name (used for Tagging) 96 | Type: String 97 | BucketName: 98 | Description: Bucket name where nested templates live 99 | Type: String 100 | 101 | ClassB: 102 | Description: Class B of VPC (10.XXX.0.0/16) 103 | Type: Number 104 | 105 | ClusterName: 106 | Description: Fargate Cluster Name (will be exported for use with resources in this project) 107 | Type: String 108 | 109 | ServiceName: 110 | Type: String 111 | Description: Name given to the service being run on the Fargate Cluster 112 | ContainerName: 113 | Type: String 114 | Description: The name of a docker image that should be run from ECR 115 | ContainerPort: 116 | Type: Number 117 | Description: What port number the application inside the docker container 118 | ContainerCpu: 119 | Type: Number 120 | Description: How much CPU to give the container. 1024 is 1 CPU 121 | ContainerMemory: 122 | Type: Number 123 | Description: How much memory in megabytes to give the container 124 | DesiredCount: 125 | Type: Number 126 | Description: How many copies of the service task to run 127 | HealthEndpoint: 128 | Type: String 129 | Description: Health Endpoint to target via Application load balancer health checks 130 | 131 | CodeRepositoryURL: 132 | Description: URL of the repo which contains CFN template. 133 | Type: String 134 | 135 | BuildspecLocation: 136 | Description: Location of buildspec configuration 137 | Type: String 138 | EnvironmentType: 139 | Description: Type of build environment to use for related builds. 140 | Type: String 141 | AllowedValues: 142 | - WINDOWS_CONTAINER 143 | - LINUX_CONTAINER 144 | ComputeType: 145 | Description: Compute resources the build project will use to build. 146 | Type: String 147 | AllowedValues: 148 | - BUILD_GENERAL1_SMALL 149 | - BUILD_GENERAL1_MEDIUM 150 | - BUILD_GENERAL1_LARGE 151 | BuildImage: 152 | Description: System Image identifier of the image to use for code build 153 | Type: String 154 | 155 | Resources: 156 | 157 | baseFargate: 158 | Type: AWS::CloudFormation::Stack 159 | Properties: 160 | Parameters: 161 | ProjectName: 162 | !Ref ProjectName 163 | ClusterName: 164 | !Ref ClusterName 165 | TemplateURL: !Sub 'https://s3.amazonaws.com/${BucketName}/resources/gpt-2-flask-api/cloudformation/base/fargate-cluster.yaml' 166 | 167 | baseNetworking: 168 | Type: AWS::CloudFormation::Stack 169 | Properties: 170 | Parameters: 171 | ClassB: 172 | !Ref ClassB 173 | TemplateURL: !Sub 'https://s3.amazonaws.com/${BucketName}/resources/gpt-2-flask-api/cloudformation/base/vpc-networking.yaml' 174 | 175 | service: 176 | Type: AWS::CloudFormation::Stack 177 | Properties: 178 | Parameters: 179 | ServiceName: 180 | !Ref ServiceName 181 | VPCId: 182 | !GetAtt [ baseNetworking, Outputs.VPC ] 183 | PublicSubnetIDs: 184 | !GetAtt [ baseNetworking, Outputs.SubnetsPublic ] 185 | PrivateSubnetIDs: 186 | !GetAtt [ baseNetworking, Outputs.SubnetsPrivate ] 187 | FargateCluster: 188 | !GetAtt [ baseFargate, Outputs.FargateCluster ] 189 | ContainerName: 190 | !Ref ContainerName 191 | ContainerPort: 192 | !Ref ContainerPort 193 | ContainerCpu: 194 | !Ref ContainerCpu 195 | ContainerMemory: 196 | !Ref ContainerMemory 197 | DesiredCount: 198 | !Ref DesiredCount 199 | HealthEndpoint: 200 | !Ref HealthEndpoint 201 | TemplateURL: !Sub 'https://s3.amazonaws.com/${BucketName}/resources/gpt-2-flask-api/cloudformation/base/fargate-service.yaml' 202 | 203 | codebuild: 204 | Type: AWS::CloudFormation::Stack 205 | Properties: 206 | Parameters: 207 | VPC: 208 | !GetAtt [ baseNetworking, Outputs.VPC ] 209 | SubnetIDs: 210 | !GetAtt [ baseNetworking, Outputs.SubnetsPrivate ] 211 | FargateCluster: 212 | !GetAtt [ baseFargate, Outputs.FargateCluster ] 213 | FargateService: 214 | !Ref ServiceName 215 | CodeRepositoryURL: 216 | !Ref CodeRepositoryURL 217 | BuildspecLocation: 218 | !Ref BuildspecLocation 219 | ImageRepoName: 220 | !Ref ContainerName 221 | EnvironmentType: 222 | !Ref EnvironmentType 223 | ComputeType: 224 | !Ref ComputeType 225 | BuildImage: 226 | !Ref BuildImage 227 | TemplateURL: !Sub 'https://s3.amazonaws.com/${BucketName}/resources/gpt-2-flask-api/cloudformation/cicd/codebuild.yaml' 228 | 229 | Outputs: 230 | 231 | ApiEndpoint: 232 | Description: API Endpoint for the Service 233 | Value: !GetAtt [ service, Outputs.EndpointUrl ] 234 | -------------------------------------------------------------------------------- /cloudformation_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BUCKET_NAME=devopstar 4 | 5 | ## Creates S3 bucket 6 | aws s3 mb s3://$BUCKET_NAME 7 | 8 | ## S3 cloudformation deployments 9 | ### Base 10 | aws s3 cp cloudformation/base/fargate-cluster.yaml s3://$BUCKET_NAME/resources/gpt-2-flask-api/cloudformation/base/fargate-cluster.yaml 11 | aws s3 cp cloudformation/base/fargate-service.yaml s3://$BUCKET_NAME/resources/gpt-2-flask-api/cloudformation/base/fargate-service.yaml 12 | aws s3 cp cloudformation/base/vpc-networking.yaml s3://$BUCKET_NAME/resources/gpt-2-flask-api/cloudformation/base/vpc-networking.yaml 13 | ### CI/CD 14 | aws s3 cp cloudformation/cicd/codebuild.yaml s3://$BUCKET_NAME/resources/gpt-2-flask-api/cloudformation/cicd/codebuild.yaml 15 | -------------------------------------------------------------------------------- /container_push.sh: -------------------------------------------------------------------------------- 1 | # Create ECR (if not already existing) 2 | aws ecr create-repository --repository-name "gpt-2-flask-api" 3 | 4 | ACCOUNT_ID=$(aws sts get-caller-identity | jq -r '.Account') 5 | $(aws ecr get-login --no-include-email --region us-east-1) 6 | 7 | docker build -t gpt-2-flask-api . 8 | docker tag gpt-2-flask-api:latest $ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/gpt-2-flask-api:latest 9 | docker push $ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/gpt-2-flask-api:latest 10 | -------------------------------------------------------------------------------- /deployment/GPT2/config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | code by TaeHwan Jung(@graykode) 3 | Original Paper and repository here : https://github.com/openai/gpt-2 4 | GPT2 Pytorch Model : https://github.com/huggingface/pytorch-pretrained-BERT 5 | ''' 6 | class GPT2Config(object): 7 | def __init__( 8 | self, 9 | vocab_size_or_config_json_file=50257, 10 | n_positions=1024, 11 | n_ctx=1024, 12 | n_embd=768, 13 | n_layer=12, 14 | n_head=12, 15 | layer_norm_epsilon=1e-5, 16 | initializer_range=0.02, 17 | ): 18 | self.vocab_size = vocab_size_or_config_json_file 19 | self.n_ctx = n_ctx 20 | self.n_positions = n_positions 21 | self.n_embd = n_embd 22 | self.n_layer = n_layer 23 | self.n_head = n_head 24 | self.layer_norm_epsilon = layer_norm_epsilon 25 | self.initializer_range = initializer_range -------------------------------------------------------------------------------- /deployment/GPT2/encoder.py: -------------------------------------------------------------------------------- 1 | """Byte pair encoding utilities""" 2 | 3 | import os 4 | import json 5 | import regex as re 6 | from functools import lru_cache 7 | 8 | @lru_cache() 9 | def bytes_to_unicode(): 10 | """ 11 | Returns list of utf-8 byte and a corresponding list of unicode strings. 12 | The reversible bpe codes work on unicode strings. 13 | This means you need a large # of unicode characters in your vocab if you want to avoid UNKs. 14 | When you're at something like a 10B token dataset you end up needing around 5K for decent coverage. 15 | This is a signficant percentage of your normal, say, 32K bpe vocab. 16 | To avoid that, we want lookup tables between utf-8 bytes and unicode strings. 17 | And avoids mapping to whitespace/control characters the bpe code barfs on. 18 | """ 19 | bs = list(range(ord("!"), ord("~")+1))+list(range(ord("¡"), ord("¬")+1))+list(range(ord("®"), ord("ÿ")+1)) 20 | cs = bs[:] 21 | n = 0 22 | for b in range(2**8): 23 | if b not in bs: 24 | bs.append(b) 25 | cs.append(2**8+n) 26 | n += 1 27 | cs = [chr(n) for n in cs] 28 | return dict(zip(bs, cs)) 29 | 30 | def get_pairs(word): 31 | """Return set of symbol pairs in a word. 32 | Word is represented as tuple of symbols (symbols being variable-length strings). 33 | """ 34 | pairs = set() 35 | prev_char = word[0] 36 | for char in word[1:]: 37 | pairs.add((prev_char, char)) 38 | prev_char = char 39 | return pairs 40 | 41 | class Encoder: 42 | def __init__(self, encoder, bpe_merges, errors='replace'): 43 | self.encoder = encoder 44 | self.decoder = {v:k for k,v in self.encoder.items()} 45 | self.errors = errors # how to handle errors in decoding 46 | self.byte_encoder = bytes_to_unicode() 47 | self.byte_decoder = {v:k for k, v in self.byte_encoder.items()} 48 | self.bpe_ranks = dict(zip(bpe_merges, range(len(bpe_merges)))) 49 | self.cache = {} 50 | 51 | # Should haved added re.IGNORECASE so BPE merges can happen for capitalized versions of contractions 52 | self.pat = re.compile(r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""") 53 | 54 | def bpe(self, token): 55 | if token in self.cache: 56 | return self.cache[token] 57 | word = tuple(token) 58 | pairs = get_pairs(word) 59 | 60 | if not pairs: 61 | return token 62 | 63 | while True: 64 | bigram = min(pairs, key = lambda pair: self.bpe_ranks.get(pair, float('inf'))) 65 | if bigram not in self.bpe_ranks: 66 | break 67 | first, second = bigram 68 | new_word = [] 69 | i = 0 70 | while i < len(word): 71 | try: 72 | j = word.index(first, i) 73 | new_word.extend(word[i:j]) 74 | i = j 75 | except: 76 | new_word.extend(word[i:]) 77 | break 78 | 79 | if word[i] == first and i < len(word)-1 and word[i+1] == second: 80 | new_word.append(first+second) 81 | i += 2 82 | else: 83 | new_word.append(word[i]) 84 | i += 1 85 | new_word = tuple(new_word) 86 | word = new_word 87 | if len(word) == 1: 88 | break 89 | else: 90 | pairs = get_pairs(word) 91 | word = ' '.join(word) 92 | self.cache[token] = word 93 | return word 94 | 95 | def encode(self, text): 96 | bpe_tokens = [] 97 | for token in re.findall(self.pat, text): 98 | token = ''.join(self.byte_encoder[b] for b in token.encode('utf-8')) 99 | bpe_tokens.extend(self.encoder[bpe_token] for bpe_token in self.bpe(token).split(' ')) 100 | return bpe_tokens 101 | 102 | def decode(self, tokens): 103 | text = ''.join([self.decoder[token] for token in tokens]) 104 | text = bytearray([self.byte_decoder[c] for c in text]).decode('utf-8', errors=self.errors) 105 | return text 106 | 107 | def get_encoder(): 108 | with open('./GPT2/encoder.json', 'r') as f: 109 | encoder = json.load(f) 110 | with open('./GPT2/vocab.bpe', 'r', encoding="utf-8") as f: 111 | bpe_data = f.read() 112 | bpe_merges = [tuple(merge_str.split()) for merge_str in bpe_data.split('\n')[1:-1]] 113 | return Encoder( 114 | encoder=encoder, 115 | bpe_merges=bpe_merges, 116 | ) -------------------------------------------------------------------------------- /deployment/GPT2/model.py: -------------------------------------------------------------------------------- 1 | ''' 2 | code by TaeHwan Jung(@graykode) 3 | Original Paper and repository here : https://github.com/openai/gpt-2 4 | GPT2 Pytorch Model : https://github.com/huggingface/pytorch-pretrained-BERT 5 | ''' 6 | import copy 7 | import torch 8 | import math 9 | import torch.nn as nn 10 | from torch.nn.parameter import Parameter 11 | 12 | def gelu(x): 13 | return 0.5 * x * (1 + torch.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * torch.pow(x, 3)))) 14 | 15 | class LayerNorm(nn.Module): 16 | def __init__(self, hidden_size, eps=1e-12): 17 | """Construct a layernorm module in the TF style (epsilon inside the square root). 18 | """ 19 | super(LayerNorm, self).__init__() 20 | self.weight = nn.Parameter(torch.ones(hidden_size)) 21 | self.bias = nn.Parameter(torch.zeros(hidden_size)) 22 | self.variance_epsilon = eps 23 | 24 | def forward(self, x): 25 | u = x.mean(-1, keepdim=True) 26 | s = (x - u).pow(2).mean(-1, keepdim=True) 27 | x = (x - u) / torch.sqrt(s + self.variance_epsilon) 28 | return self.weight * x + self.bias 29 | 30 | class Conv1D(nn.Module): 31 | def __init__(self, nf, nx): 32 | super(Conv1D, self).__init__() 33 | self.nf = nf 34 | w = torch.empty(nx, nf) 35 | nn.init.normal_(w, std=0.02) 36 | self.weight = Parameter(w) 37 | self.bias = Parameter(torch.zeros(nf)) 38 | 39 | def forward(self, x): 40 | size_out = x.size()[:-1] + (self.nf,) 41 | x = torch.addmm(self.bias, x.view(-1, x.size(-1)), self.weight) 42 | x = x.view(*size_out) 43 | return x 44 | 45 | class Attention(nn.Module): 46 | def __init__(self, nx, n_ctx, config, scale=False): 47 | super(Attention, self).__init__() 48 | n_state = nx # in Attention: n_state=768 (nx=n_embd) 49 | # [switch nx => n_state from Block to Attention to keep identical to TF implem] 50 | assert n_state % config.n_head == 0 51 | self.register_buffer("bias", torch.tril(torch.ones(n_ctx, n_ctx)).view(1, 1, n_ctx, n_ctx)) 52 | self.n_head = config.n_head 53 | self.split_size = n_state 54 | self.scale = scale 55 | self.c_attn = Conv1D(n_state * 3, nx) 56 | self.c_proj = Conv1D(n_state, nx) 57 | 58 | def _attn(self, q, k, v): 59 | w = torch.matmul(q, k) 60 | if self.scale: 61 | w = w / math.sqrt(v.size(-1)) 62 | nd, ns = w.size(-2), w.size(-1) 63 | b = self.bias[:, :, ns-nd:ns, :ns] 64 | w = w * b - 1e10 * (1 - b) 65 | w = nn.Softmax(dim=-1)(w) 66 | return torch.matmul(w, v) 67 | 68 | def merge_heads(self, x): 69 | x = x.permute(0, 2, 1, 3).contiguous() 70 | new_x_shape = x.size()[:-2] + (x.size(-2) * x.size(-1),) 71 | return x.view(*new_x_shape) # in Tensorflow implem: fct merge_states 72 | 73 | def split_heads(self, x, k=False): 74 | new_x_shape = x.size()[:-1] + (self.n_head, x.size(-1) // self.n_head) 75 | x = x.view(*new_x_shape) # in Tensorflow implem: fct split_states 76 | if k: 77 | return x.permute(0, 2, 3, 1) # (batch, head, head_features, seq_length) 78 | else: 79 | return x.permute(0, 2, 1, 3) # (batch, head, seq_length, head_features) 80 | 81 | def forward(self, x, layer_past=None): 82 | x = self.c_attn(x) 83 | query, key, value = x.split(self.split_size, dim=2) 84 | query = self.split_heads(query) 85 | key = self.split_heads(key, k=True) 86 | value = self.split_heads(value) 87 | if layer_past is not None: 88 | past_key, past_value = layer_past[0].transpose(-2, -1), layer_past[1] # transpose back cf below 89 | key = torch.cat((past_key, key), dim=-1) 90 | value = torch.cat((past_value, value), dim=-2) 91 | present = torch.stack((key.transpose(-2, -1), value)) # transpose to have same shapes for stacking 92 | a = self._attn(query, key, value) 93 | a = self.merge_heads(a) 94 | a = self.c_proj(a) 95 | return a, present 96 | 97 | class MLP(nn.Module): 98 | def __init__(self, n_state, config): # in MLP: n_state=3072 (4 * n_embd) 99 | super(MLP, self).__init__() 100 | nx = config.n_embd 101 | self.c_fc = Conv1D(n_state, nx) 102 | self.c_proj = Conv1D(nx, n_state) 103 | self.act = gelu 104 | 105 | def forward(self, x): 106 | h = self.act(self.c_fc(x)) 107 | h2 = self.c_proj(h) 108 | return h2 109 | 110 | class Block(nn.Module): 111 | def __init__(self, n_ctx, config, scale=False): 112 | super(Block, self).__init__() 113 | nx = config.n_embd 114 | self.ln_1 = LayerNorm(nx, eps=config.layer_norm_epsilon) 115 | self.attn = Attention(nx, n_ctx, config, scale) 116 | self.ln_2 = LayerNorm(nx, eps=config.layer_norm_epsilon) 117 | self.mlp = MLP(4 * nx, config) 118 | 119 | def forward(self, x, layer_past=None): 120 | a, present = self.attn(self.ln_1(x), layer_past=layer_past) 121 | x = x + a 122 | m = self.mlp(self.ln_2(x)) 123 | x = x + m 124 | return x, present 125 | 126 | class GPT2Model(nn.Module): 127 | def __init__(self, config): 128 | super(GPT2Model, self).__init__() 129 | self.n_layer = config.n_layer 130 | self.n_embd = config.n_embd 131 | self.n_vocab = config.vocab_size 132 | 133 | self.wte = nn.Embedding(config.vocab_size, config.n_embd) 134 | self.wpe = nn.Embedding(config.n_positions, config.n_embd) 135 | block = Block(config.n_ctx, config, scale=True) 136 | self.h = nn.ModuleList([copy.deepcopy(block) for _ in range(config.n_layer)]) 137 | self.ln_f = LayerNorm(config.n_embd, eps=config.layer_norm_epsilon) 138 | 139 | def set_embeddings_weights(self, model_embeddings_weights): 140 | embed_shape = model_embeddings_weights.shape 141 | self.decoder = nn.Linear(embed_shape[1], embed_shape[0], bias=False) 142 | self.decoder.weight = model_embeddings_weights # Tied weights 143 | 144 | def forward(self, input_ids, position_ids=None, token_type_ids=None, past=None): 145 | if past is None: 146 | past_length = 0 147 | past = [None] * len(self.h) 148 | else: 149 | past_length = past[0][0].size(-2) 150 | if position_ids is None: 151 | position_ids = torch.arange(past_length, input_ids.size(-1) + past_length, dtype=torch.long, 152 | device=input_ids.device) 153 | position_ids = position_ids.unsqueeze(0).expand_as(input_ids) 154 | 155 | input_shape = input_ids.size() 156 | input_ids = input_ids.view(-1, input_ids.size(-1)) 157 | position_ids = position_ids.view(-1, position_ids.size(-1)) 158 | 159 | inputs_embeds = self.wte(input_ids) 160 | position_embeds = self.wpe(position_ids) 161 | if token_type_ids is not None: 162 | token_type_ids = token_type_ids.view(-1, token_type_ids.size(-1)) 163 | token_type_embeds = self.wte(token_type_ids) 164 | else: 165 | token_type_embeds = 0 166 | hidden_states = inputs_embeds + position_embeds + token_type_embeds 167 | presents = [] 168 | for block, layer_past in zip(self.h, past): 169 | hidden_states, present = block(hidden_states, layer_past) 170 | presents.append(present) 171 | hidden_states = self.ln_f(hidden_states) 172 | output_shape = input_shape + (hidden_states.size(-1),) 173 | return hidden_states.view(*output_shape), presents 174 | 175 | class GPT2LMHead(nn.Module): 176 | def __init__(self, model_embeddings_weights, config): 177 | super(GPT2LMHead, self).__init__() 178 | self.n_embd = config.n_embd 179 | self.set_embeddings_weights(model_embeddings_weights) 180 | 181 | def set_embeddings_weights(self, model_embeddings_weights): 182 | embed_shape = model_embeddings_weights.shape 183 | self.decoder = nn.Linear(embed_shape[1], embed_shape[0], bias=False) 184 | self.decoder.weight = model_embeddings_weights # Tied weights 185 | 186 | def forward(self, hidden_state): 187 | # Truncated Language modeling logits (we remove the last token) 188 | # h_trunc = h[:, :-1].contiguous().view(-1, self.n_embd) 189 | lm_logits = self.decoder(hidden_state) 190 | return lm_logits 191 | 192 | class GPT2LMHeadModel(nn.Module): 193 | def __init__(self, config): 194 | super(GPT2LMHeadModel, self).__init__() 195 | self.transformer = GPT2Model(config) 196 | self.lm_head = GPT2LMHead(self.transformer.wte.weight, config) 197 | 198 | def set_tied(self): 199 | """ Make sure we are sharing the embeddings 200 | """ 201 | self.lm_head.set_embeddings_weights(self.transformer.wte.weight) 202 | 203 | def forward(self, input_ids, position_ids=None, token_type_ids=None, lm_labels=None, past=None): 204 | hidden_states, presents = self.transformer(input_ids, position_ids, token_type_ids, past) 205 | lm_logits = self.lm_head(hidden_states) 206 | if lm_labels is not None: 207 | loss_fct = nn.CrossEntropyLoss(ignore_index=-1) 208 | loss = loss_fct(lm_logits.view(-1, lm_logits.size(-1)), lm_labels.view(-1)) 209 | return loss 210 | return lm_logits, presents -------------------------------------------------------------------------------- /deployment/GPT2/sample.py: -------------------------------------------------------------------------------- 1 | ''' 2 | code by TaeHwan Jung(@graykode) 3 | Original Paper and repository here : https://github.com/openai/gpt-2 4 | GPT2 Pytorch Model : https://github.com/huggingface/pytorch-pretrained-BERT 5 | ''' 6 | import torch 7 | import torch.nn.functional as F 8 | from tqdm import trange 9 | 10 | def top_k_logits(logits, k): 11 | if k == 0: 12 | return logits 13 | values, _ = torch.topk(logits, k) 14 | min_values = values[:, -1] 15 | return torch.where(logits < min_values, torch.ones_like(logits, dtype=logits.dtype) * -1e10, logits) 16 | 17 | def sample_sequence(model, length, start_token=None, batch_size=None, context=None, temperature=1, top_k=0, device='cuda', sample=True): 18 | if start_token is None: 19 | assert context is not None, 'Specify exactly one of start_token and context!' 20 | context = torch.tensor(context, device=device, dtype=torch.long).unsqueeze(0).repeat(batch_size, 1) 21 | else: 22 | assert context is None, 'Specify exactly one of start_token and context!' 23 | context = torch.full((batch_size, 1), start_token, device=device, dtype=torch.long) 24 | prev = context 25 | output = context 26 | past = None 27 | with torch.no_grad(): 28 | for i in trange(length): 29 | logits, past = model(prev, past=past) 30 | logits = logits[:, -1, :] / temperature 31 | logits = top_k_logits(logits, k=top_k) 32 | log_probs = F.softmax(logits, dim=-1) 33 | if sample: 34 | prev = torch.multinomial(log_probs, num_samples=1) 35 | else: 36 | _, prev = torch.topk(log_probs, k=1, dim=-1) 37 | output = torch.cat((output, prev), dim=1) 38 | return output -------------------------------------------------------------------------------- /deployment/GPT2/utils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | code by TaeHwan Jung(@graykode) 3 | Original Paper and repository here : https://github.com/openai/gpt-2 4 | GPT2 Pytorch Model : https://github.com/huggingface/pytorch-pretrained-BERT 5 | ''' 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | def load_weight(model, state_dict): 11 | old_keys = [] 12 | new_keys = [] 13 | for key in state_dict.keys(): 14 | new_key = None 15 | if key.endswith(".g"): 16 | new_key = key[:-2] + ".weight" 17 | elif key.endswith(".b"): 18 | new_key = key[:-2] + ".bias" 19 | elif key.endswith(".w"): 20 | new_key = key[:-2] + ".weight" 21 | if new_key: 22 | old_keys.append(key) 23 | new_keys.append(new_key) 24 | for old_key, new_key in zip(old_keys, new_keys): 25 | state_dict[new_key] = state_dict.pop(old_key) 26 | 27 | missing_keys = [] 28 | unexpected_keys = [] 29 | error_msgs = [] 30 | # copy state_dict so _load_from_state_dict can modify it 31 | metadata = getattr(state_dict, "_metadata", None) 32 | state_dict = state_dict.copy() 33 | if metadata is not None: 34 | state_dict._metadata = metadata 35 | 36 | def load(module, prefix=""): 37 | local_metadata = {} if metadata is None else metadata.get(prefix[:-1], {}) 38 | module._load_from_state_dict( 39 | state_dict, prefix, local_metadata, True, missing_keys, unexpected_keys, error_msgs 40 | ) 41 | for name, child in module._modules.items(): 42 | if child is not None: 43 | load(child, prefix + name + ".") 44 | 45 | start_model = model 46 | if hasattr(model, "transformer") and all(not s.startswith('transformer.') for s in state_dict.keys()): 47 | start_model = model.transformer 48 | load(start_model, prefix="") 49 | 50 | # Make sure we are still sharing the output and input embeddings after loading weights 51 | model.set_tied() 52 | return model -------------------------------------------------------------------------------- /deployment/run_server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | import torch 3 | import random 4 | import numpy as np 5 | from utils import header, add_content, box 6 | from wtforms import Form, TextField, validators, SubmitField 7 | from GPT2.model import (GPT2LMHeadModel) 8 | from GPT2.utils import load_weight 9 | from GPT2.config import GPT2Config 10 | from GPT2.sample import sample_sequence 11 | from GPT2.encoder import get_encoder 12 | 13 | 14 | # Create app 15 | app = Flask(__name__) 16 | 17 | 18 | class ReusableForm(Form): 19 | """User entry form for entering specifics for generation""" 20 | # Starting seed 21 | seed = TextField("Enter a seed sentence:", validators=[ 22 | validators.InputRequired()]) 23 | 24 | # Submit button 25 | submit = SubmitField("Enter") 26 | 27 | 28 | def text_generator( 29 | seed, 30 | unconditional=False, 31 | nsamples=1, 32 | batch_size=-1, 33 | length=-1, 34 | temperature=0.7, 35 | top_k=40): 36 | 37 | enc = get_encoder() 38 | context_tokens = enc.encode(seed) 39 | 40 | if batch_size == -1: 41 | batch_size = 1 42 | assert nsamples % batch_size == 0 43 | 44 | if length == -1: 45 | length = config.n_ctx // 2 46 | elif length > config.n_ctx: 47 | raise ValueError("Can't get samples longer than window size: %s" % config.n_ctx) 48 | 49 | out = sample_sequence( 50 | model=model, 51 | length=length, 52 | context=context_tokens if not unconditional else None, 53 | start_token=enc.encoder['<|endoftext|>'] if unconditional else None, 54 | batch_size=batch_size, 55 | temperature=temperature, 56 | top_k=top_k, 57 | device=device 58 | ) 59 | 60 | text = '' 61 | 62 | out = out[:, len(context_tokens):].tolist() 63 | for i in range(batch_size): 64 | text += enc.decode(out[i]) 65 | 66 | html = '' 67 | html = add_content(html, header( 68 | 'Input Seed ', color='black', gen_text='Network Output')) 69 | html = add_content(html, box(seed, text)) 70 | return f'
{html}
' 71 | 72 | 73 | def load_gpt2_model(): 74 | """Load in the pre-trained model""" 75 | 76 | # Load Model File 77 | state_dict = torch.load('../models/gpt2-pytorch_model.bin', map_location='cpu' if not torch.cuda.is_available() else None) 78 | 79 | seed = random.randint(0, 2147483647) 80 | np.random.seed(seed) 81 | torch.random.manual_seed(seed) 82 | torch.cuda.manual_seed(seed) 83 | 84 | global device 85 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 86 | 87 | # Load Model 88 | global config 89 | config = GPT2Config() 90 | global model 91 | model = GPT2LMHeadModel(config) 92 | model = load_weight(model, state_dict) 93 | model.to(device) 94 | model.eval() 95 | 96 | 97 | # Home page 98 | @app.route("/", methods=['GET', 'POST']) 99 | def home(): 100 | """Home page of app with form""" 101 | # Create form 102 | form = ReusableForm(request.form) 103 | 104 | # On form entry and all conditions met 105 | if request.method == 'POST' and form.validate(): 106 | # Extract information 107 | seed = request.form['seed'] 108 | # Generate a random sequence 109 | return render_template('seeded.html', seed=seed, input=text_generator(seed=seed)) 110 | # Send template information to index.html 111 | return render_template('index.html', form=form) 112 | 113 | 114 | if __name__ == "__main__": 115 | print(("* Loading model and Flask starting server..." 116 | "please wait until server has fully started")) 117 | load_gpt2_model() 118 | # Run app 119 | app.run(host="0.0.0.0", port=5000) 120 | -------------------------------------------------------------------------------- /deployment/static/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | color: #444; 6 | } 7 | 8 | 9 | .container { 10 | text-align: center; 11 | background-color: ivory; 12 | width: 940px; 13 | border-width: 4px; 14 | margin: 100px auto 0; 15 | } 16 | 17 | .container::before { 18 | background-image: url('/static/images/background.jpg'); 19 | background-size: cover; 20 | content: ""; 21 | display: block; 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | width: 100%; 26 | height: 100%; 27 | z-index: -2; 28 | opacity: 0.4; 29 | } 30 | 31 | div.jumbo { 32 | padding: 10px 0 30px 0; 33 | background-color: #eeeeee; 34 | -webkit-border-radius: 6px; 35 | -moz-border-radius: 6px; 36 | border-radius: 6px; 37 | } 38 | 39 | 40 | h1 { 41 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 42 | text-align: center; 43 | color: darkslategray; 44 | display: block; 45 | font-size: 48px; 46 | margin: 8px 0; 47 | font-weight: bold; 48 | } 49 | 50 | h2 { 51 | font-size: 3em; 52 | margin-top: 40px; 53 | text-align: center; 54 | letter-spacing: -2px; 55 | } 56 | 57 | h3 { 58 | font-size: 1.7em; 59 | font-weight: 100; 60 | margin-top: 30px; 61 | text-align: center; 62 | letter-spacing: -1px; 63 | color: #999; 64 | } 65 | 66 | ul { 67 | list-style-type: none; 68 | margin: 0; 69 | padding: 0; 70 | overflow: hidden; 71 | background-color: #333; 72 | } 73 | 74 | li { 75 | float: left; 76 | } 77 | 78 | li a { 79 | display: block; 80 | color: white; 81 | text-align: center; 82 | padding: 14px 16px; 83 | text-decoration: none; 84 | } 85 | 86 | /* Change the link color to #111 (black) on hover */ 87 | li a:hover { 88 | background-color: #111; 89 | } 90 | 91 | form label { 92 | font-family: initial; 93 | color: #A1BD83; 94 | font-size: 26px; 95 | font-weight: bold; 96 | display: block; 97 | padding: 10px 0; 98 | } 99 | 100 | form input#seed, form input#diversity, form input#words { 101 | height: 50px; 102 | width: 75%; 103 | background-color: #fafafa; 104 | -webkit-border-radius: 3px; 105 | -moz-border-radius: 3px; 106 | border-radius: 3px; 107 | border: 1px solid #cccccc; 108 | padding: 5px; 109 | font-size: 24px; 110 | margin: 12px; 111 | } 112 | 113 | form textarea#message { 114 | width: 75%; 115 | height: 100px; 116 | background-color: #fafafa; 117 | -webkit-border-radius: 3px; 118 | -moz-border-radius: 3px; 119 | border-radius: 3px; 120 | border: 1px solid #cccccc; 121 | margin-bottom: 10px; 122 | padding: 5px; 123 | font-size: 1.1em; 124 | } 125 | 126 | form input#submit { 127 | height: 80px; 128 | width: 30%; 129 | margin: auto; 130 | display: -webkit-box; 131 | -webkit-border-radius: 3px; 132 | -moz-border-radius: 3px; 133 | border-radius: 10px; 134 | border: 1px solid #d8d8d8; 135 | padding: 10px; 136 | font-weight: bold; 137 | font-size: 22px; 138 | text-align: center; 139 | color: #000000; 140 | background-color: #f4f4f4; 141 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f4f4f4), color-stop(100%, #e5e5e5)); 142 | background-image: -webkit-linear-gradient(top, #f4f4f4, #e5e5e5); 143 | background-image: -moz-linear-gradient(top, #f4f4f4, #e5e5e5); 144 | background-image: -ms-linear-gradient(top, #f4f4f4, #e5e5e5); 145 | background-image: -o-linear-gradient(top, #f4f4f4, #e5e5e5); 146 | background-image: linear-gradient(top, #f4f4f4, #e5e5e5); 147 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr=#f4f4f4, endColorstr=#e5e5e5); 148 | } 149 | 150 | form input#submit:hover { 151 | cursor: pointer; 152 | border: 1px solid #c1c1c1; 153 | background-color: #dbdbdb; 154 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #dbdbdb), color-stop(100%, #cccccc)); 155 | background-image: -webkit-linear-gradient(top, #dbdbdb, #cccccc); 156 | background-image: -moz-linear-gradient(top, #dbdbdb, #cccccc); 157 | background-image: -ms-linear-gradient(top, #dbdbdb, #cccccc); 158 | background-image: -o-linear-gradient(top, #dbdbdb, #cccccc); 159 | background-image: linear-gradient(top, #dbdbdb, #cccccc); 160 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr=#dbdbdb, endColorstr=#cccccc); 161 | } 162 | 163 | /* Message flashing */ 164 | .flash { 165 | background-color: #FBB0B0; 166 | padding: 10px; 167 | width: 400px; 168 | display: table; 169 | margin: auto; 170 | } -------------------------------------------------------------------------------- /deployment/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t04glovern/gpt-2-flask-api/a4930a5308e6f8aa37ac20995032343131da185e/deployment/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /deployment/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t04glovern/gpt-2-flask-api/a4930a5308e6f8aa37ac20995032343131da185e/deployment/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /deployment/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t04glovern/gpt-2-flask-api/a4930a5308e6f8aa37ac20995032343131da185e/deployment/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /deployment/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t04glovern/gpt-2-flask-api/a4930a5308e6f8aa37ac20995032343131da185e/deployment/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /deployment/static/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t04glovern/gpt-2-flask-api/a4930a5308e6f8aa37ac20995032343131da185e/deployment/static/images/background.jpg -------------------------------------------------------------------------------- /deployment/static/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | 7 | if (typeof jQuery === 'undefined') { 8 | throw new Error('Bootstrap\'s JavaScript requires jQuery') 9 | } 10 | 11 | +function ($) { 12 | 'use strict'; 13 | var version = $.fn.jquery.split(' ')[0].split('.') 14 | if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) { 15 | throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher') 16 | } 17 | }(jQuery); 18 | 19 | /* ======================================================================== 20 | * Bootstrap: transition.js v3.3.5 21 | * http://getbootstrap.com/javascript/#transitions 22 | * ======================================================================== 23 | * Copyright 2011-2015 Twitter, Inc. 24 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 25 | * ======================================================================== */ 26 | 27 | 28 | +function ($) { 29 | 'use strict'; 30 | 31 | // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) 32 | // ============================================================ 33 | 34 | function transitionEnd() { 35 | var el = document.createElement('bootstrap') 36 | 37 | var transEndEventNames = { 38 | WebkitTransition : 'webkitTransitionEnd', 39 | MozTransition : 'transitionend', 40 | OTransition : 'oTransitionEnd otransitionend', 41 | transition : 'transitionend' 42 | } 43 | 44 | for (var name in transEndEventNames) { 45 | if (el.style[name] !== undefined) { 46 | return { end: transEndEventNames[name] } 47 | } 48 | } 49 | 50 | return false // explicit for ie8 ( ._.) 51 | } 52 | 53 | // http://blog.alexmaccaw.com/css-transitions 54 | $.fn.emulateTransitionEnd = function (duration) { 55 | var called = false 56 | var $el = this 57 | $(this).one('bsTransitionEnd', function () { called = true }) 58 | var callback = function () { if (!called) $($el).trigger($.support.transition.end) } 59 | setTimeout(callback, duration) 60 | return this 61 | } 62 | 63 | $(function () { 64 | $.support.transition = transitionEnd() 65 | 66 | if (!$.support.transition) return 67 | 68 | $.event.special.bsTransitionEnd = { 69 | bindType: $.support.transition.end, 70 | delegateType: $.support.transition.end, 71 | handle: function (e) { 72 | if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) 73 | } 74 | } 75 | }) 76 | 77 | }(jQuery); 78 | 79 | /* ======================================================================== 80 | * Bootstrap: alert.js v3.3.5 81 | * http://getbootstrap.com/javascript/#alerts 82 | * ======================================================================== 83 | * Copyright 2011-2015 Twitter, Inc. 84 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 85 | * ======================================================================== */ 86 | 87 | 88 | +function ($) { 89 | 'use strict'; 90 | 91 | // ALERT CLASS DEFINITION 92 | // ====================== 93 | 94 | var dismiss = '[data-dismiss="alert"]' 95 | var Alert = function (el) { 96 | $(el).on('click', dismiss, this.close) 97 | } 98 | 99 | Alert.VERSION = '3.3.5' 100 | 101 | Alert.TRANSITION_DURATION = 150 102 | 103 | Alert.prototype.close = function (e) { 104 | var $this = $(this) 105 | var selector = $this.attr('data-target') 106 | 107 | if (!selector) { 108 | selector = $this.attr('href') 109 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 110 | } 111 | 112 | var $parent = $(selector) 113 | 114 | if (e) e.preventDefault() 115 | 116 | if (!$parent.length) { 117 | $parent = $this.closest('.alert') 118 | } 119 | 120 | $parent.trigger(e = $.Event('close.bs.alert')) 121 | 122 | if (e.isDefaultPrevented()) return 123 | 124 | $parent.removeClass('in') 125 | 126 | function removeElement() { 127 | // detach from parent, fire event then clean up data 128 | $parent.detach().trigger('closed.bs.alert').remove() 129 | } 130 | 131 | $.support.transition && $parent.hasClass('fade') ? 132 | $parent 133 | .one('bsTransitionEnd', removeElement) 134 | .emulateTransitionEnd(Alert.TRANSITION_DURATION) : 135 | removeElement() 136 | } 137 | 138 | 139 | // ALERT PLUGIN DEFINITION 140 | // ======================= 141 | 142 | function Plugin(option) { 143 | return this.each(function () { 144 | var $this = $(this) 145 | var data = $this.data('bs.alert') 146 | 147 | if (!data) $this.data('bs.alert', (data = new Alert(this))) 148 | if (typeof option == 'string') data[option].call($this) 149 | }) 150 | } 151 | 152 | var old = $.fn.alert 153 | 154 | $.fn.alert = Plugin 155 | $.fn.alert.Constructor = Alert 156 | 157 | 158 | // ALERT NO CONFLICT 159 | // ================= 160 | 161 | $.fn.alert.noConflict = function () { 162 | $.fn.alert = old 163 | return this 164 | } 165 | 166 | 167 | // ALERT DATA-API 168 | // ============== 169 | 170 | $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) 171 | 172 | }(jQuery); 173 | 174 | /* ======================================================================== 175 | * Bootstrap: button.js v3.3.5 176 | * http://getbootstrap.com/javascript/#buttons 177 | * ======================================================================== 178 | * Copyright 2011-2015 Twitter, Inc. 179 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 180 | * ======================================================================== */ 181 | 182 | 183 | +function ($) { 184 | 'use strict'; 185 | 186 | // BUTTON PUBLIC CLASS DEFINITION 187 | // ============================== 188 | 189 | var Button = function (element, options) { 190 | this.$element = $(element) 191 | this.options = $.extend({}, Button.DEFAULTS, options) 192 | this.isLoading = false 193 | } 194 | 195 | Button.VERSION = '3.3.5' 196 | 197 | Button.DEFAULTS = { 198 | loadingText: 'loading...' 199 | } 200 | 201 | Button.prototype.setState = function (state) { 202 | var d = 'disabled' 203 | var $el = this.$element 204 | var val = $el.is('input') ? 'val' : 'html' 205 | var data = $el.data() 206 | 207 | state += 'Text' 208 | 209 | if (data.resetText == null) $el.data('resetText', $el[val]()) 210 | 211 | // push to event loop to allow forms to submit 212 | setTimeout($.proxy(function () { 213 | $el[val](data[state] == null ? this.options[state] : data[state]) 214 | 215 | if (state == 'loadingText') { 216 | this.isLoading = true 217 | $el.addClass(d).attr(d, d) 218 | } else if (this.isLoading) { 219 | this.isLoading = false 220 | $el.removeClass(d).removeAttr(d) 221 | } 222 | }, this), 0) 223 | } 224 | 225 | Button.prototype.toggle = function () { 226 | var changed = true 227 | var $parent = this.$element.closest('[data-toggle="buttons"]') 228 | 229 | if ($parent.length) { 230 | var $input = this.$element.find('input') 231 | if ($input.prop('type') == 'radio') { 232 | if ($input.prop('checked')) changed = false 233 | $parent.find('.active').removeClass('active') 234 | this.$element.addClass('active') 235 | } else if ($input.prop('type') == 'checkbox') { 236 | if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false 237 | this.$element.toggleClass('active') 238 | } 239 | $input.prop('checked', this.$element.hasClass('active')) 240 | if (changed) $input.trigger('change') 241 | } else { 242 | this.$element.attr('aria-pressed', !this.$element.hasClass('active')) 243 | this.$element.toggleClass('active') 244 | } 245 | } 246 | 247 | 248 | // BUTTON PLUGIN DEFINITION 249 | // ======================== 250 | 251 | function Plugin(option) { 252 | return this.each(function () { 253 | var $this = $(this) 254 | var data = $this.data('bs.button') 255 | var options = typeof option == 'object' && option 256 | 257 | if (!data) $this.data('bs.button', (data = new Button(this, options))) 258 | 259 | if (option == 'toggle') data.toggle() 260 | else if (option) data.setState(option) 261 | }) 262 | } 263 | 264 | var old = $.fn.button 265 | 266 | $.fn.button = Plugin 267 | $.fn.button.Constructor = Button 268 | 269 | 270 | // BUTTON NO CONFLICT 271 | // ================== 272 | 273 | $.fn.button.noConflict = function () { 274 | $.fn.button = old 275 | return this 276 | } 277 | 278 | 279 | // BUTTON DATA-API 280 | // =============== 281 | 282 | $(document) 283 | .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { 284 | var $btn = $(e.target) 285 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 286 | Plugin.call($btn, 'toggle') 287 | if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() 288 | }) 289 | .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { 290 | $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) 291 | }) 292 | 293 | }(jQuery); 294 | 295 | /* ======================================================================== 296 | * Bootstrap: carousel.js v3.3.5 297 | * http://getbootstrap.com/javascript/#carousel 298 | * ======================================================================== 299 | * Copyright 2011-2015 Twitter, Inc. 300 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 301 | * ======================================================================== */ 302 | 303 | 304 | +function ($) { 305 | 'use strict'; 306 | 307 | // CAROUSEL CLASS DEFINITION 308 | // ========================= 309 | 310 | var Carousel = function (element, options) { 311 | this.$element = $(element) 312 | this.$indicators = this.$element.find('.carousel-indicators') 313 | this.options = options 314 | this.paused = null 315 | this.sliding = null 316 | this.interval = null 317 | this.$active = null 318 | this.$items = null 319 | 320 | this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) 321 | 322 | this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element 323 | .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) 324 | .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) 325 | } 326 | 327 | Carousel.VERSION = '3.3.5' 328 | 329 | Carousel.TRANSITION_DURATION = 600 330 | 331 | Carousel.DEFAULTS = { 332 | interval: 5000, 333 | pause: 'hover', 334 | wrap: true, 335 | keyboard: true 336 | } 337 | 338 | Carousel.prototype.keydown = function (e) { 339 | if (/input|textarea/i.test(e.target.tagName)) return 340 | switch (e.which) { 341 | case 37: this.prev(); break 342 | case 39: this.next(); break 343 | default: return 344 | } 345 | 346 | e.preventDefault() 347 | } 348 | 349 | Carousel.prototype.cycle = function (e) { 350 | e || (this.paused = false) 351 | 352 | this.interval && clearInterval(this.interval) 353 | 354 | this.options.interval 355 | && !this.paused 356 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) 357 | 358 | return this 359 | } 360 | 361 | Carousel.prototype.getItemIndex = function (item) { 362 | this.$items = item.parent().children('.item') 363 | return this.$items.index(item || this.$active) 364 | } 365 | 366 | Carousel.prototype.getItemForDirection = function (direction, active) { 367 | var activeIndex = this.getItemIndex(active) 368 | var willWrap = (direction == 'prev' && activeIndex === 0) 369 | || (direction == 'next' && activeIndex == (this.$items.length - 1)) 370 | if (willWrap && !this.options.wrap) return active 371 | var delta = direction == 'prev' ? -1 : 1 372 | var itemIndex = (activeIndex + delta) % this.$items.length 373 | return this.$items.eq(itemIndex) 374 | } 375 | 376 | Carousel.prototype.to = function (pos) { 377 | var that = this 378 | var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) 379 | 380 | if (pos > (this.$items.length - 1) || pos < 0) return 381 | 382 | if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" 383 | if (activeIndex == pos) return this.pause().cycle() 384 | 385 | return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) 386 | } 387 | 388 | Carousel.prototype.pause = function (e) { 389 | e || (this.paused = true) 390 | 391 | if (this.$element.find('.next, .prev').length && $.support.transition) { 392 | this.$element.trigger($.support.transition.end) 393 | this.cycle(true) 394 | } 395 | 396 | this.interval = clearInterval(this.interval) 397 | 398 | return this 399 | } 400 | 401 | Carousel.prototype.next = function () { 402 | if (this.sliding) return 403 | return this.slide('next') 404 | } 405 | 406 | Carousel.prototype.prev = function () { 407 | if (this.sliding) return 408 | return this.slide('prev') 409 | } 410 | 411 | Carousel.prototype.slide = function (type, next) { 412 | var $active = this.$element.find('.item.active') 413 | var $next = next || this.getItemForDirection(type, $active) 414 | var isCycling = this.interval 415 | var direction = type == 'next' ? 'left' : 'right' 416 | var that = this 417 | 418 | if ($next.hasClass('active')) return (this.sliding = false) 419 | 420 | var relatedTarget = $next[0] 421 | var slideEvent = $.Event('slide.bs.carousel', { 422 | relatedTarget: relatedTarget, 423 | direction: direction 424 | }) 425 | this.$element.trigger(slideEvent) 426 | if (slideEvent.isDefaultPrevented()) return 427 | 428 | this.sliding = true 429 | 430 | isCycling && this.pause() 431 | 432 | if (this.$indicators.length) { 433 | this.$indicators.find('.active').removeClass('active') 434 | var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) 435 | $nextIndicator && $nextIndicator.addClass('active') 436 | } 437 | 438 | var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" 439 | if ($.support.transition && this.$element.hasClass('slide')) { 440 | $next.addClass(type) 441 | $next[0].offsetWidth // force reflow 442 | $active.addClass(direction) 443 | $next.addClass(direction) 444 | $active 445 | .one('bsTransitionEnd', function () { 446 | $next.removeClass([type, direction].join(' ')).addClass('active') 447 | $active.removeClass(['active', direction].join(' ')) 448 | that.sliding = false 449 | setTimeout(function () { 450 | that.$element.trigger(slidEvent) 451 | }, 0) 452 | }) 453 | .emulateTransitionEnd(Carousel.TRANSITION_DURATION) 454 | } else { 455 | $active.removeClass('active') 456 | $next.addClass('active') 457 | this.sliding = false 458 | this.$element.trigger(slidEvent) 459 | } 460 | 461 | isCycling && this.cycle() 462 | 463 | return this 464 | } 465 | 466 | 467 | // CAROUSEL PLUGIN DEFINITION 468 | // ========================== 469 | 470 | function Plugin(option) { 471 | return this.each(function () { 472 | var $this = $(this) 473 | var data = $this.data('bs.carousel') 474 | var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) 475 | var action = typeof option == 'string' ? option : options.slide 476 | 477 | if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) 478 | if (typeof option == 'number') data.to(option) 479 | else if (action) data[action]() 480 | else if (options.interval) data.pause().cycle() 481 | }) 482 | } 483 | 484 | var old = $.fn.carousel 485 | 486 | $.fn.carousel = Plugin 487 | $.fn.carousel.Constructor = Carousel 488 | 489 | 490 | // CAROUSEL NO CONFLICT 491 | // ==================== 492 | 493 | $.fn.carousel.noConflict = function () { 494 | $.fn.carousel = old 495 | return this 496 | } 497 | 498 | 499 | // CAROUSEL DATA-API 500 | // ================= 501 | 502 | var clickHandler = function (e) { 503 | var href 504 | var $this = $(this) 505 | var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 506 | if (!$target.hasClass('carousel')) return 507 | var options = $.extend({}, $target.data(), $this.data()) 508 | var slideIndex = $this.attr('data-slide-to') 509 | if (slideIndex) options.interval = false 510 | 511 | Plugin.call($target, options) 512 | 513 | if (slideIndex) { 514 | $target.data('bs.carousel').to(slideIndex) 515 | } 516 | 517 | e.preventDefault() 518 | } 519 | 520 | $(document) 521 | .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) 522 | .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) 523 | 524 | $(window).on('load', function () { 525 | $('[data-ride="carousel"]').each(function () { 526 | var $carousel = $(this) 527 | Plugin.call($carousel, $carousel.data()) 528 | }) 529 | }) 530 | 531 | }(jQuery); 532 | 533 | /* ======================================================================== 534 | * Bootstrap: collapse.js v3.3.5 535 | * http://getbootstrap.com/javascript/#collapse 536 | * ======================================================================== 537 | * Copyright 2011-2015 Twitter, Inc. 538 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 539 | * ======================================================================== */ 540 | 541 | 542 | +function ($) { 543 | 'use strict'; 544 | 545 | // COLLAPSE PUBLIC CLASS DEFINITION 546 | // ================================ 547 | 548 | var Collapse = function (element, options) { 549 | this.$element = $(element) 550 | this.options = $.extend({}, Collapse.DEFAULTS, options) 551 | this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + 552 | '[data-toggle="collapse"][data-target="#' + element.id + '"]') 553 | this.transitioning = null 554 | 555 | if (this.options.parent) { 556 | this.$parent = this.getParent() 557 | } else { 558 | this.addAriaAndCollapsedClass(this.$element, this.$trigger) 559 | } 560 | 561 | if (this.options.toggle) this.toggle() 562 | } 563 | 564 | Collapse.VERSION = '3.3.5' 565 | 566 | Collapse.TRANSITION_DURATION = 350 567 | 568 | Collapse.DEFAULTS = { 569 | toggle: true 570 | } 571 | 572 | Collapse.prototype.dimension = function () { 573 | var hasWidth = this.$element.hasClass('width') 574 | return hasWidth ? 'width' : 'height' 575 | } 576 | 577 | Collapse.prototype.show = function () { 578 | if (this.transitioning || this.$element.hasClass('in')) return 579 | 580 | var activesData 581 | var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') 582 | 583 | if (actives && actives.length) { 584 | activesData = actives.data('bs.collapse') 585 | if (activesData && activesData.transitioning) return 586 | } 587 | 588 | var startEvent = $.Event('show.bs.collapse') 589 | this.$element.trigger(startEvent) 590 | if (startEvent.isDefaultPrevented()) return 591 | 592 | if (actives && actives.length) { 593 | Plugin.call(actives, 'hide') 594 | activesData || actives.data('bs.collapse', null) 595 | } 596 | 597 | var dimension = this.dimension() 598 | 599 | this.$element 600 | .removeClass('collapse') 601 | .addClass('collapsing')[dimension](0) 602 | .attr('aria-expanded', true) 603 | 604 | this.$trigger 605 | .removeClass('collapsed') 606 | .attr('aria-expanded', true) 607 | 608 | this.transitioning = 1 609 | 610 | var complete = function () { 611 | this.$element 612 | .removeClass('collapsing') 613 | .addClass('collapse in')[dimension]('') 614 | this.transitioning = 0 615 | this.$element 616 | .trigger('shown.bs.collapse') 617 | } 618 | 619 | if (!$.support.transition) return complete.call(this) 620 | 621 | var scrollSize = $.camelCase(['scroll', dimension].join('-')) 622 | 623 | this.$element 624 | .one('bsTransitionEnd', $.proxy(complete, this)) 625 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) 626 | } 627 | 628 | Collapse.prototype.hide = function () { 629 | if (this.transitioning || !this.$element.hasClass('in')) return 630 | 631 | var startEvent = $.Event('hide.bs.collapse') 632 | this.$element.trigger(startEvent) 633 | if (startEvent.isDefaultPrevented()) return 634 | 635 | var dimension = this.dimension() 636 | 637 | this.$element[dimension](this.$element[dimension]())[0].offsetHeight 638 | 639 | this.$element 640 | .addClass('collapsing') 641 | .removeClass('collapse in') 642 | .attr('aria-expanded', false) 643 | 644 | this.$trigger 645 | .addClass('collapsed') 646 | .attr('aria-expanded', false) 647 | 648 | this.transitioning = 1 649 | 650 | var complete = function () { 651 | this.transitioning = 0 652 | this.$element 653 | .removeClass('collapsing') 654 | .addClass('collapse') 655 | .trigger('hidden.bs.collapse') 656 | } 657 | 658 | if (!$.support.transition) return complete.call(this) 659 | 660 | this.$element 661 | [dimension](0) 662 | .one('bsTransitionEnd', $.proxy(complete, this)) 663 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION) 664 | } 665 | 666 | Collapse.prototype.toggle = function () { 667 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 668 | } 669 | 670 | Collapse.prototype.getParent = function () { 671 | return $(this.options.parent) 672 | .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') 673 | .each($.proxy(function (i, element) { 674 | var $element = $(element) 675 | this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) 676 | }, this)) 677 | .end() 678 | } 679 | 680 | Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { 681 | var isOpen = $element.hasClass('in') 682 | 683 | $element.attr('aria-expanded', isOpen) 684 | $trigger 685 | .toggleClass('collapsed', !isOpen) 686 | .attr('aria-expanded', isOpen) 687 | } 688 | 689 | function getTargetFromTrigger($trigger) { 690 | var href 691 | var target = $trigger.attr('data-target') 692 | || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 693 | 694 | return $(target) 695 | } 696 | 697 | 698 | // COLLAPSE PLUGIN DEFINITION 699 | // ========================== 700 | 701 | function Plugin(option) { 702 | return this.each(function () { 703 | var $this = $(this) 704 | var data = $this.data('bs.collapse') 705 | var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) 706 | 707 | if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false 708 | if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) 709 | if (typeof option == 'string') data[option]() 710 | }) 711 | } 712 | 713 | var old = $.fn.collapse 714 | 715 | $.fn.collapse = Plugin 716 | $.fn.collapse.Constructor = Collapse 717 | 718 | 719 | // COLLAPSE NO CONFLICT 720 | // ==================== 721 | 722 | $.fn.collapse.noConflict = function () { 723 | $.fn.collapse = old 724 | return this 725 | } 726 | 727 | 728 | // COLLAPSE DATA-API 729 | // ================= 730 | 731 | $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { 732 | var $this = $(this) 733 | 734 | if (!$this.attr('data-target')) e.preventDefault() 735 | 736 | var $target = getTargetFromTrigger($this) 737 | var data = $target.data('bs.collapse') 738 | var option = data ? 'toggle' : $this.data() 739 | 740 | Plugin.call($target, option) 741 | }) 742 | 743 | }(jQuery); 744 | 745 | /* ======================================================================== 746 | * Bootstrap: dropdown.js v3.3.5 747 | * http://getbootstrap.com/javascript/#dropdowns 748 | * ======================================================================== 749 | * Copyright 2011-2015 Twitter, Inc. 750 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 751 | * ======================================================================== */ 752 | 753 | 754 | +function ($) { 755 | 'use strict'; 756 | 757 | // DROPDOWN CLASS DEFINITION 758 | // ========================= 759 | 760 | var backdrop = '.dropdown-backdrop' 761 | var toggle = '[data-toggle="dropdown"]' 762 | var Dropdown = function (element) { 763 | $(element).on('click.bs.dropdown', this.toggle) 764 | } 765 | 766 | Dropdown.VERSION = '3.3.5' 767 | 768 | function getParent($this) { 769 | var selector = $this.attr('data-target') 770 | 771 | if (!selector) { 772 | selector = $this.attr('href') 773 | selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 774 | } 775 | 776 | var $parent = selector && $(selector) 777 | 778 | return $parent && $parent.length ? $parent : $this.parent() 779 | } 780 | 781 | function clearMenus(e) { 782 | if (e && e.which === 3) return 783 | $(backdrop).remove() 784 | $(toggle).each(function () { 785 | var $this = $(this) 786 | var $parent = getParent($this) 787 | var relatedTarget = { relatedTarget: this } 788 | 789 | if (!$parent.hasClass('open')) return 790 | 791 | if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return 792 | 793 | $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) 794 | 795 | if (e.isDefaultPrevented()) return 796 | 797 | $this.attr('aria-expanded', 'false') 798 | $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) 799 | }) 800 | } 801 | 802 | Dropdown.prototype.toggle = function (e) { 803 | var $this = $(this) 804 | 805 | if ($this.is('.disabled, :disabled')) return 806 | 807 | var $parent = getParent($this) 808 | var isActive = $parent.hasClass('open') 809 | 810 | clearMenus() 811 | 812 | if (!isActive) { 813 | if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { 814 | // if mobile we use a backdrop because click events don't delegate 815 | $(document.createElement('div')) 816 | .addClass('dropdown-backdrop') 817 | .insertAfter($(this)) 818 | .on('click', clearMenus) 819 | } 820 | 821 | var relatedTarget = { relatedTarget: this } 822 | $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) 823 | 824 | if (e.isDefaultPrevented()) return 825 | 826 | $this 827 | .trigger('focus') 828 | .attr('aria-expanded', 'true') 829 | 830 | $parent 831 | .toggleClass('open') 832 | .trigger('shown.bs.dropdown', relatedTarget) 833 | } 834 | 835 | return false 836 | } 837 | 838 | Dropdown.prototype.keydown = function (e) { 839 | if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return 840 | 841 | var $this = $(this) 842 | 843 | e.preventDefault() 844 | e.stopPropagation() 845 | 846 | if ($this.is('.disabled, :disabled')) return 847 | 848 | var $parent = getParent($this) 849 | var isActive = $parent.hasClass('open') 850 | 851 | if (!isActive && e.which != 27 || isActive && e.which == 27) { 852 | if (e.which == 27) $parent.find(toggle).trigger('focus') 853 | return $this.trigger('click') 854 | } 855 | 856 | var desc = ' li:not(.disabled):visible a' 857 | var $items = $parent.find('.dropdown-menu' + desc) 858 | 859 | if (!$items.length) return 860 | 861 | var index = $items.index(e.target) 862 | 863 | if (e.which == 38 && index > 0) index-- // up 864 | if (e.which == 40 && index < $items.length - 1) index++ // down 865 | if (!~index) index = 0 866 | 867 | $items.eq(index).trigger('focus') 868 | } 869 | 870 | 871 | // DROPDOWN PLUGIN DEFINITION 872 | // ========================== 873 | 874 | function Plugin(option) { 875 | return this.each(function () { 876 | var $this = $(this) 877 | var data = $this.data('bs.dropdown') 878 | 879 | if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) 880 | if (typeof option == 'string') data[option].call($this) 881 | }) 882 | } 883 | 884 | var old = $.fn.dropdown 885 | 886 | $.fn.dropdown = Plugin 887 | $.fn.dropdown.Constructor = Dropdown 888 | 889 | 890 | // DROPDOWN NO CONFLICT 891 | // ==================== 892 | 893 | $.fn.dropdown.noConflict = function () { 894 | $.fn.dropdown = old 895 | return this 896 | } 897 | 898 | 899 | // APPLY TO STANDARD DROPDOWN ELEMENTS 900 | // =================================== 901 | 902 | $(document) 903 | .on('click.bs.dropdown.data-api', clearMenus) 904 | .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) 905 | .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) 906 | .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) 907 | .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) 908 | 909 | }(jQuery); 910 | 911 | /* ======================================================================== 912 | * Bootstrap: modal.js v3.3.5 913 | * http://getbootstrap.com/javascript/#modals 914 | * ======================================================================== 915 | * Copyright 2011-2015 Twitter, Inc. 916 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 917 | * ======================================================================== */ 918 | 919 | 920 | +function ($) { 921 | 'use strict'; 922 | 923 | // MODAL CLASS DEFINITION 924 | // ====================== 925 | 926 | var Modal = function (element, options) { 927 | this.options = options 928 | this.$body = $(document.body) 929 | this.$element = $(element) 930 | this.$dialog = this.$element.find('.modal-dialog') 931 | this.$backdrop = null 932 | this.isShown = null 933 | this.originalBodyPad = null 934 | this.scrollbarWidth = 0 935 | this.ignoreBackdropClick = false 936 | 937 | if (this.options.remote) { 938 | this.$element 939 | .find('.modal-content') 940 | .load(this.options.remote, $.proxy(function () { 941 | this.$element.trigger('loaded.bs.modal') 942 | }, this)) 943 | } 944 | } 945 | 946 | Modal.VERSION = '3.3.5' 947 | 948 | Modal.TRANSITION_DURATION = 300 949 | Modal.BACKDROP_TRANSITION_DURATION = 150 950 | 951 | Modal.DEFAULTS = { 952 | backdrop: true, 953 | keyboard: true, 954 | show: true 955 | } 956 | 957 | Modal.prototype.toggle = function (_relatedTarget) { 958 | return this.isShown ? this.hide() : this.show(_relatedTarget) 959 | } 960 | 961 | Modal.prototype.show = function (_relatedTarget) { 962 | var that = this 963 | var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) 964 | 965 | this.$element.trigger(e) 966 | 967 | if (this.isShown || e.isDefaultPrevented()) return 968 | 969 | this.isShown = true 970 | 971 | this.checkScrollbar() 972 | this.setScrollbar() 973 | this.$body.addClass('modal-open') 974 | 975 | this.escape() 976 | this.resize() 977 | 978 | this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) 979 | 980 | this.$dialog.on('mousedown.dismiss.bs.modal', function () { 981 | that.$element.one('mouseup.dismiss.bs.modal', function (e) { 982 | if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true 983 | }) 984 | }) 985 | 986 | this.backdrop(function () { 987 | var transition = $.support.transition && that.$element.hasClass('fade') 988 | 989 | if (!that.$element.parent().length) { 990 | that.$element.appendTo(that.$body) // don't move modals dom position 991 | } 992 | 993 | that.$element 994 | .show() 995 | .scrollTop(0) 996 | 997 | that.adjustDialog() 998 | 999 | if (transition) { 1000 | that.$element[0].offsetWidth // force reflow 1001 | } 1002 | 1003 | that.$element.addClass('in') 1004 | 1005 | that.enforceFocus() 1006 | 1007 | var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) 1008 | 1009 | transition ? 1010 | that.$dialog // wait for modal to slide in 1011 | .one('bsTransitionEnd', function () { 1012 | that.$element.trigger('focus').trigger(e) 1013 | }) 1014 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 1015 | that.$element.trigger('focus').trigger(e) 1016 | }) 1017 | } 1018 | 1019 | Modal.prototype.hide = function (e) { 1020 | if (e) e.preventDefault() 1021 | 1022 | e = $.Event('hide.bs.modal') 1023 | 1024 | this.$element.trigger(e) 1025 | 1026 | if (!this.isShown || e.isDefaultPrevented()) return 1027 | 1028 | this.isShown = false 1029 | 1030 | this.escape() 1031 | this.resize() 1032 | 1033 | $(document).off('focusin.bs.modal') 1034 | 1035 | this.$element 1036 | .removeClass('in') 1037 | .off('click.dismiss.bs.modal') 1038 | .off('mouseup.dismiss.bs.modal') 1039 | 1040 | this.$dialog.off('mousedown.dismiss.bs.modal') 1041 | 1042 | $.support.transition && this.$element.hasClass('fade') ? 1043 | this.$element 1044 | .one('bsTransitionEnd', $.proxy(this.hideModal, this)) 1045 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 1046 | this.hideModal() 1047 | } 1048 | 1049 | Modal.prototype.enforceFocus = function () { 1050 | $(document) 1051 | .off('focusin.bs.modal') // guard against infinite focus loop 1052 | .on('focusin.bs.modal', $.proxy(function (e) { 1053 | if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { 1054 | this.$element.trigger('focus') 1055 | } 1056 | }, this)) 1057 | } 1058 | 1059 | Modal.prototype.escape = function () { 1060 | if (this.isShown && this.options.keyboard) { 1061 | this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { 1062 | e.which == 27 && this.hide() 1063 | }, this)) 1064 | } else if (!this.isShown) { 1065 | this.$element.off('keydown.dismiss.bs.modal') 1066 | } 1067 | } 1068 | 1069 | Modal.prototype.resize = function () { 1070 | if (this.isShown) { 1071 | $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) 1072 | } else { 1073 | $(window).off('resize.bs.modal') 1074 | } 1075 | } 1076 | 1077 | Modal.prototype.hideModal = function () { 1078 | var that = this 1079 | this.$element.hide() 1080 | this.backdrop(function () { 1081 | that.$body.removeClass('modal-open') 1082 | that.resetAdjustments() 1083 | that.resetScrollbar() 1084 | that.$element.trigger('hidden.bs.modal') 1085 | }) 1086 | } 1087 | 1088 | Modal.prototype.removeBackdrop = function () { 1089 | this.$backdrop && this.$backdrop.remove() 1090 | this.$backdrop = null 1091 | } 1092 | 1093 | Modal.prototype.backdrop = function (callback) { 1094 | var that = this 1095 | var animate = this.$element.hasClass('fade') ? 'fade' : '' 1096 | 1097 | if (this.isShown && this.options.backdrop) { 1098 | var doAnimate = $.support.transition && animate 1099 | 1100 | this.$backdrop = $(document.createElement('div')) 1101 | .addClass('modal-backdrop ' + animate) 1102 | .appendTo(this.$body) 1103 | 1104 | this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { 1105 | if (this.ignoreBackdropClick) { 1106 | this.ignoreBackdropClick = false 1107 | return 1108 | } 1109 | if (e.target !== e.currentTarget) return 1110 | this.options.backdrop == 'static' 1111 | ? this.$element[0].focus() 1112 | : this.hide() 1113 | }, this)) 1114 | 1115 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow 1116 | 1117 | this.$backdrop.addClass('in') 1118 | 1119 | if (!callback) return 1120 | 1121 | doAnimate ? 1122 | this.$backdrop 1123 | .one('bsTransitionEnd', callback) 1124 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 1125 | callback() 1126 | 1127 | } else if (!this.isShown && this.$backdrop) { 1128 | this.$backdrop.removeClass('in') 1129 | 1130 | var callbackRemove = function () { 1131 | that.removeBackdrop() 1132 | callback && callback() 1133 | } 1134 | $.support.transition && this.$element.hasClass('fade') ? 1135 | this.$backdrop 1136 | .one('bsTransitionEnd', callbackRemove) 1137 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 1138 | callbackRemove() 1139 | 1140 | } else if (callback) { 1141 | callback() 1142 | } 1143 | } 1144 | 1145 | // these following methods are used to handle overflowing modals 1146 | 1147 | Modal.prototype.handleUpdate = function () { 1148 | this.adjustDialog() 1149 | } 1150 | 1151 | Modal.prototype.adjustDialog = function () { 1152 | var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight 1153 | 1154 | this.$element.css({ 1155 | paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', 1156 | paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' 1157 | }) 1158 | } 1159 | 1160 | Modal.prototype.resetAdjustments = function () { 1161 | this.$element.css({ 1162 | paddingLeft: '', 1163 | paddingRight: '' 1164 | }) 1165 | } 1166 | 1167 | Modal.prototype.checkScrollbar = function () { 1168 | var fullWindowWidth = window.innerWidth 1169 | if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 1170 | var documentElementRect = document.documentElement.getBoundingClientRect() 1171 | fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) 1172 | } 1173 | this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth 1174 | this.scrollbarWidth = this.measureScrollbar() 1175 | } 1176 | 1177 | Modal.prototype.setScrollbar = function () { 1178 | var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) 1179 | this.originalBodyPad = document.body.style.paddingRight || '' 1180 | if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) 1181 | } 1182 | 1183 | Modal.prototype.resetScrollbar = function () { 1184 | this.$body.css('padding-right', this.originalBodyPad) 1185 | } 1186 | 1187 | Modal.prototype.measureScrollbar = function () { // thx walsh 1188 | var scrollDiv = document.createElement('div') 1189 | scrollDiv.className = 'modal-scrollbar-measure' 1190 | this.$body.append(scrollDiv) 1191 | var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth 1192 | this.$body[0].removeChild(scrollDiv) 1193 | return scrollbarWidth 1194 | } 1195 | 1196 | 1197 | // MODAL PLUGIN DEFINITION 1198 | // ======================= 1199 | 1200 | function Plugin(option, _relatedTarget) { 1201 | return this.each(function () { 1202 | var $this = $(this) 1203 | var data = $this.data('bs.modal') 1204 | var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) 1205 | 1206 | if (!data) $this.data('bs.modal', (data = new Modal(this, options))) 1207 | if (typeof option == 'string') data[option](_relatedTarget) 1208 | else if (options.show) data.show(_relatedTarget) 1209 | }) 1210 | } 1211 | 1212 | var old = $.fn.modal 1213 | 1214 | $.fn.modal = Plugin 1215 | $.fn.modal.Constructor = Modal 1216 | 1217 | 1218 | // MODAL NO CONFLICT 1219 | // ================= 1220 | 1221 | $.fn.modal.noConflict = function () { 1222 | $.fn.modal = old 1223 | return this 1224 | } 1225 | 1226 | 1227 | // MODAL DATA-API 1228 | // ============== 1229 | 1230 | $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { 1231 | var $this = $(this) 1232 | var href = $this.attr('href') 1233 | var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 1234 | var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) 1235 | 1236 | if ($this.is('a')) e.preventDefault() 1237 | 1238 | $target.one('show.bs.modal', function (showEvent) { 1239 | if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown 1240 | $target.one('hidden.bs.modal', function () { 1241 | $this.is(':visible') && $this.trigger('focus') 1242 | }) 1243 | }) 1244 | Plugin.call($target, option, this) 1245 | }) 1246 | 1247 | }(jQuery); 1248 | 1249 | /* ======================================================================== 1250 | * Bootstrap: tooltip.js v3.3.5 1251 | * http://getbootstrap.com/javascript/#tooltip 1252 | * Inspired by the original jQuery.tipsy by Jason Frame 1253 | * ======================================================================== 1254 | * Copyright 2011-2015 Twitter, Inc. 1255 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1256 | * ======================================================================== */ 1257 | 1258 | 1259 | +function ($) { 1260 | 'use strict'; 1261 | 1262 | // TOOLTIP PUBLIC CLASS DEFINITION 1263 | // =============================== 1264 | 1265 | var Tooltip = function (element, options) { 1266 | this.type = null 1267 | this.options = null 1268 | this.enabled = null 1269 | this.timeout = null 1270 | this.hoverState = null 1271 | this.$element = null 1272 | this.inState = null 1273 | 1274 | this.init('tooltip', element, options) 1275 | } 1276 | 1277 | Tooltip.VERSION = '3.3.5' 1278 | 1279 | Tooltip.TRANSITION_DURATION = 150 1280 | 1281 | Tooltip.DEFAULTS = { 1282 | animation: true, 1283 | placement: 'top', 1284 | selector: false, 1285 | template: '', 1286 | trigger: 'hover focus', 1287 | title: '', 1288 | delay: 0, 1289 | html: false, 1290 | container: false, 1291 | viewport: { 1292 | selector: 'body', 1293 | padding: 0 1294 | } 1295 | } 1296 | 1297 | Tooltip.prototype.init = function (type, element, options) { 1298 | this.enabled = true 1299 | this.type = type 1300 | this.$element = $(element) 1301 | this.options = this.getOptions(options) 1302 | this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) 1303 | this.inState = { click: false, hover: false, focus: false } 1304 | 1305 | if (this.$element[0] instanceof document.constructor && !this.options.selector) { 1306 | throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') 1307 | } 1308 | 1309 | var triggers = this.options.trigger.split(' ') 1310 | 1311 | for (var i = triggers.length; i--;) { 1312 | var trigger = triggers[i] 1313 | 1314 | if (trigger == 'click') { 1315 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) 1316 | } else if (trigger != 'manual') { 1317 | var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' 1318 | var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' 1319 | 1320 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) 1321 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) 1322 | } 1323 | } 1324 | 1325 | this.options.selector ? 1326 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : 1327 | this.fixTitle() 1328 | } 1329 | 1330 | Tooltip.prototype.getDefaults = function () { 1331 | return Tooltip.DEFAULTS 1332 | } 1333 | 1334 | Tooltip.prototype.getOptions = function (options) { 1335 | options = $.extend({}, this.getDefaults(), this.$element.data(), options) 1336 | 1337 | if (options.delay && typeof options.delay == 'number') { 1338 | options.delay = { 1339 | show: options.delay, 1340 | hide: options.delay 1341 | } 1342 | } 1343 | 1344 | return options 1345 | } 1346 | 1347 | Tooltip.prototype.getDelegateOptions = function () { 1348 | var options = {} 1349 | var defaults = this.getDefaults() 1350 | 1351 | this._options && $.each(this._options, function (key, value) { 1352 | if (defaults[key] != value) options[key] = value 1353 | }) 1354 | 1355 | return options 1356 | } 1357 | 1358 | Tooltip.prototype.enter = function (obj) { 1359 | var self = obj instanceof this.constructor ? 1360 | obj : $(obj.currentTarget).data('bs.' + this.type) 1361 | 1362 | if (!self) { 1363 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) 1364 | $(obj.currentTarget).data('bs.' + this.type, self) 1365 | } 1366 | 1367 | if (obj instanceof $.Event) { 1368 | self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true 1369 | } 1370 | 1371 | if (self.tip().hasClass('in') || self.hoverState == 'in') { 1372 | self.hoverState = 'in' 1373 | return 1374 | } 1375 | 1376 | clearTimeout(self.timeout) 1377 | 1378 | self.hoverState = 'in' 1379 | 1380 | if (!self.options.delay || !self.options.delay.show) return self.show() 1381 | 1382 | self.timeout = setTimeout(function () { 1383 | if (self.hoverState == 'in') self.show() 1384 | }, self.options.delay.show) 1385 | } 1386 | 1387 | Tooltip.prototype.isInStateTrue = function () { 1388 | for (var key in this.inState) { 1389 | if (this.inState[key]) return true 1390 | } 1391 | 1392 | return false 1393 | } 1394 | 1395 | Tooltip.prototype.leave = function (obj) { 1396 | var self = obj instanceof this.constructor ? 1397 | obj : $(obj.currentTarget).data('bs.' + this.type) 1398 | 1399 | if (!self) { 1400 | self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) 1401 | $(obj.currentTarget).data('bs.' + this.type, self) 1402 | } 1403 | 1404 | if (obj instanceof $.Event) { 1405 | self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false 1406 | } 1407 | 1408 | if (self.isInStateTrue()) return 1409 | 1410 | clearTimeout(self.timeout) 1411 | 1412 | self.hoverState = 'out' 1413 | 1414 | if (!self.options.delay || !self.options.delay.hide) return self.hide() 1415 | 1416 | self.timeout = setTimeout(function () { 1417 | if (self.hoverState == 'out') self.hide() 1418 | }, self.options.delay.hide) 1419 | } 1420 | 1421 | Tooltip.prototype.show = function () { 1422 | var e = $.Event('show.bs.' + this.type) 1423 | 1424 | if (this.hasContent() && this.enabled) { 1425 | this.$element.trigger(e) 1426 | 1427 | var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) 1428 | if (e.isDefaultPrevented() || !inDom) return 1429 | var that = this 1430 | 1431 | var $tip = this.tip() 1432 | 1433 | var tipId = this.getUID(this.type) 1434 | 1435 | this.setContent() 1436 | $tip.attr('id', tipId) 1437 | this.$element.attr('aria-describedby', tipId) 1438 | 1439 | if (this.options.animation) $tip.addClass('fade') 1440 | 1441 | var placement = typeof this.options.placement == 'function' ? 1442 | this.options.placement.call(this, $tip[0], this.$element[0]) : 1443 | this.options.placement 1444 | 1445 | var autoToken = /\s?auto?\s?/i 1446 | var autoPlace = autoToken.test(placement) 1447 | if (autoPlace) placement = placement.replace(autoToken, '') || 'top' 1448 | 1449 | $tip 1450 | .detach() 1451 | .css({ top: 0, left: 0, display: 'block' }) 1452 | .addClass(placement) 1453 | .data('bs.' + this.type, this) 1454 | 1455 | this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) 1456 | this.$element.trigger('inserted.bs.' + this.type) 1457 | 1458 | var pos = this.getPosition() 1459 | var actualWidth = $tip[0].offsetWidth 1460 | var actualHeight = $tip[0].offsetHeight 1461 | 1462 | if (autoPlace) { 1463 | var orgPlacement = placement 1464 | var viewportDim = this.getPosition(this.$viewport) 1465 | 1466 | placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : 1467 | placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : 1468 | placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : 1469 | placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : 1470 | placement 1471 | 1472 | $tip 1473 | .removeClass(orgPlacement) 1474 | .addClass(placement) 1475 | } 1476 | 1477 | var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) 1478 | 1479 | this.applyPlacement(calculatedOffset, placement) 1480 | 1481 | var complete = function () { 1482 | var prevHoverState = that.hoverState 1483 | that.$element.trigger('shown.bs.' + that.type) 1484 | that.hoverState = null 1485 | 1486 | if (prevHoverState == 'out') that.leave(that) 1487 | } 1488 | 1489 | $.support.transition && this.$tip.hasClass('fade') ? 1490 | $tip 1491 | .one('bsTransitionEnd', complete) 1492 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : 1493 | complete() 1494 | } 1495 | } 1496 | 1497 | Tooltip.prototype.applyPlacement = function (offset, placement) { 1498 | var $tip = this.tip() 1499 | var width = $tip[0].offsetWidth 1500 | var height = $tip[0].offsetHeight 1501 | 1502 | // manually read margins because getBoundingClientRect includes difference 1503 | var marginTop = parseInt($tip.css('margin-top'), 10) 1504 | var marginLeft = parseInt($tip.css('margin-left'), 10) 1505 | 1506 | // we must check for NaN for ie 8/9 1507 | if (isNaN(marginTop)) marginTop = 0 1508 | if (isNaN(marginLeft)) marginLeft = 0 1509 | 1510 | offset.top += marginTop 1511 | offset.left += marginLeft 1512 | 1513 | // $.fn.offset doesn't round pixel values 1514 | // so we use setOffset directly with our own function B-0 1515 | $.offset.setOffset($tip[0], $.extend({ 1516 | using: function (props) { 1517 | $tip.css({ 1518 | top: Math.round(props.top), 1519 | left: Math.round(props.left) 1520 | }) 1521 | } 1522 | }, offset), 0) 1523 | 1524 | $tip.addClass('in') 1525 | 1526 | // check to see if placing tip in new offset caused the tip to resize itself 1527 | var actualWidth = $tip[0].offsetWidth 1528 | var actualHeight = $tip[0].offsetHeight 1529 | 1530 | if (placement == 'top' && actualHeight != height) { 1531 | offset.top = offset.top + height - actualHeight 1532 | } 1533 | 1534 | var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) 1535 | 1536 | if (delta.left) offset.left += delta.left 1537 | else offset.top += delta.top 1538 | 1539 | var isVertical = /top|bottom/.test(placement) 1540 | var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight 1541 | var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' 1542 | 1543 | $tip.offset(offset) 1544 | this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) 1545 | } 1546 | 1547 | Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { 1548 | this.arrow() 1549 | .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') 1550 | .css(isVertical ? 'top' : 'left', '') 1551 | } 1552 | 1553 | Tooltip.prototype.setContent = function () { 1554 | var $tip = this.tip() 1555 | var title = this.getTitle() 1556 | 1557 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) 1558 | $tip.removeClass('fade in top bottom left right') 1559 | } 1560 | 1561 | Tooltip.prototype.hide = function (callback) { 1562 | var that = this 1563 | var $tip = $(this.$tip) 1564 | var e = $.Event('hide.bs.' + this.type) 1565 | 1566 | function complete() { 1567 | if (that.hoverState != 'in') $tip.detach() 1568 | that.$element 1569 | .removeAttr('aria-describedby') 1570 | .trigger('hidden.bs.' + that.type) 1571 | callback && callback() 1572 | } 1573 | 1574 | this.$element.trigger(e) 1575 | 1576 | if (e.isDefaultPrevented()) return 1577 | 1578 | $tip.removeClass('in') 1579 | 1580 | $.support.transition && $tip.hasClass('fade') ? 1581 | $tip 1582 | .one('bsTransitionEnd', complete) 1583 | .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : 1584 | complete() 1585 | 1586 | this.hoverState = null 1587 | 1588 | return this 1589 | } 1590 | 1591 | Tooltip.prototype.fixTitle = function () { 1592 | var $e = this.$element 1593 | if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { 1594 | $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') 1595 | } 1596 | } 1597 | 1598 | Tooltip.prototype.hasContent = function () { 1599 | return this.getTitle() 1600 | } 1601 | 1602 | Tooltip.prototype.getPosition = function ($element) { 1603 | $element = $element || this.$element 1604 | 1605 | var el = $element[0] 1606 | var isBody = el.tagName == 'BODY' 1607 | 1608 | var elRect = el.getBoundingClientRect() 1609 | if (elRect.width == null) { 1610 | // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 1611 | elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) 1612 | } 1613 | var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() 1614 | var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } 1615 | var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null 1616 | 1617 | return $.extend({}, elRect, scroll, outerDims, elOffset) 1618 | } 1619 | 1620 | Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { 1621 | return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : 1622 | placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : 1623 | placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : 1624 | /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } 1625 | 1626 | } 1627 | 1628 | Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { 1629 | var delta = { top: 0, left: 0 } 1630 | if (!this.$viewport) return delta 1631 | 1632 | var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 1633 | var viewportDimensions = this.getPosition(this.$viewport) 1634 | 1635 | if (/right|left/.test(placement)) { 1636 | var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll 1637 | var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight 1638 | if (topEdgeOffset < viewportDimensions.top) { // top overflow 1639 | delta.top = viewportDimensions.top - topEdgeOffset 1640 | } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow 1641 | delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset 1642 | } 1643 | } else { 1644 | var leftEdgeOffset = pos.left - viewportPadding 1645 | var rightEdgeOffset = pos.left + viewportPadding + actualWidth 1646 | if (leftEdgeOffset < viewportDimensions.left) { // left overflow 1647 | delta.left = viewportDimensions.left - leftEdgeOffset 1648 | } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow 1649 | delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset 1650 | } 1651 | } 1652 | 1653 | return delta 1654 | } 1655 | 1656 | Tooltip.prototype.getTitle = function () { 1657 | var title 1658 | var $e = this.$element 1659 | var o = this.options 1660 | 1661 | title = $e.attr('data-original-title') 1662 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) 1663 | 1664 | return title 1665 | } 1666 | 1667 | Tooltip.prototype.getUID = function (prefix) { 1668 | do prefix += ~~(Math.random() * 1000000) 1669 | while (document.getElementById(prefix)) 1670 | return prefix 1671 | } 1672 | 1673 | Tooltip.prototype.tip = function () { 1674 | if (!this.$tip) { 1675 | this.$tip = $(this.options.template) 1676 | if (this.$tip.length != 1) { 1677 | throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') 1678 | } 1679 | } 1680 | return this.$tip 1681 | } 1682 | 1683 | Tooltip.prototype.arrow = function () { 1684 | return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) 1685 | } 1686 | 1687 | Tooltip.prototype.enable = function () { 1688 | this.enabled = true 1689 | } 1690 | 1691 | Tooltip.prototype.disable = function () { 1692 | this.enabled = false 1693 | } 1694 | 1695 | Tooltip.prototype.toggleEnabled = function () { 1696 | this.enabled = !this.enabled 1697 | } 1698 | 1699 | Tooltip.prototype.toggle = function (e) { 1700 | var self = this 1701 | if (e) { 1702 | self = $(e.currentTarget).data('bs.' + this.type) 1703 | if (!self) { 1704 | self = new this.constructor(e.currentTarget, this.getDelegateOptions()) 1705 | $(e.currentTarget).data('bs.' + this.type, self) 1706 | } 1707 | } 1708 | 1709 | if (e) { 1710 | self.inState.click = !self.inState.click 1711 | if (self.isInStateTrue()) self.enter(self) 1712 | else self.leave(self) 1713 | } else { 1714 | self.tip().hasClass('in') ? self.leave(self) : self.enter(self) 1715 | } 1716 | } 1717 | 1718 | Tooltip.prototype.destroy = function () { 1719 | var that = this 1720 | clearTimeout(this.timeout) 1721 | this.hide(function () { 1722 | that.$element.off('.' + that.type).removeData('bs.' + that.type) 1723 | if (that.$tip) { 1724 | that.$tip.detach() 1725 | } 1726 | that.$tip = null 1727 | that.$arrow = null 1728 | that.$viewport = null 1729 | }) 1730 | } 1731 | 1732 | 1733 | // TOOLTIP PLUGIN DEFINITION 1734 | // ========================= 1735 | 1736 | function Plugin(option) { 1737 | return this.each(function () { 1738 | var $this = $(this) 1739 | var data = $this.data('bs.tooltip') 1740 | var options = typeof option == 'object' && option 1741 | 1742 | if (!data && /destroy|hide/.test(option)) return 1743 | if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) 1744 | if (typeof option == 'string') data[option]() 1745 | }) 1746 | } 1747 | 1748 | var old = $.fn.tooltip 1749 | 1750 | $.fn.tooltip = Plugin 1751 | $.fn.tooltip.Constructor = Tooltip 1752 | 1753 | 1754 | // TOOLTIP NO CONFLICT 1755 | // =================== 1756 | 1757 | $.fn.tooltip.noConflict = function () { 1758 | $.fn.tooltip = old 1759 | return this 1760 | } 1761 | 1762 | }(jQuery); 1763 | 1764 | /* ======================================================================== 1765 | * Bootstrap: popover.js v3.3.5 1766 | * http://getbootstrap.com/javascript/#popovers 1767 | * ======================================================================== 1768 | * Copyright 2011-2015 Twitter, Inc. 1769 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1770 | * ======================================================================== */ 1771 | 1772 | 1773 | +function ($) { 1774 | 'use strict'; 1775 | 1776 | // POPOVER PUBLIC CLASS DEFINITION 1777 | // =============================== 1778 | 1779 | var Popover = function (element, options) { 1780 | this.init('popover', element, options) 1781 | } 1782 | 1783 | if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') 1784 | 1785 | Popover.VERSION = '3.3.5' 1786 | 1787 | Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { 1788 | placement: 'right', 1789 | trigger: 'click', 1790 | content: '', 1791 | template: '' 1792 | }) 1793 | 1794 | 1795 | // NOTE: POPOVER EXTENDS tooltip.js 1796 | // ================================ 1797 | 1798 | Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) 1799 | 1800 | Popover.prototype.constructor = Popover 1801 | 1802 | Popover.prototype.getDefaults = function () { 1803 | return Popover.DEFAULTS 1804 | } 1805 | 1806 | Popover.prototype.setContent = function () { 1807 | var $tip = this.tip() 1808 | var title = this.getTitle() 1809 | var content = this.getContent() 1810 | 1811 | $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) 1812 | $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events 1813 | this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' 1814 | ](content) 1815 | 1816 | $tip.removeClass('fade top bottom left right in') 1817 | 1818 | // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do 1819 | // this manually by checking the contents. 1820 | if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() 1821 | } 1822 | 1823 | Popover.prototype.hasContent = function () { 1824 | return this.getTitle() || this.getContent() 1825 | } 1826 | 1827 | Popover.prototype.getContent = function () { 1828 | var $e = this.$element 1829 | var o = this.options 1830 | 1831 | return $e.attr('data-content') 1832 | || (typeof o.content == 'function' ? 1833 | o.content.call($e[0]) : 1834 | o.content) 1835 | } 1836 | 1837 | Popover.prototype.arrow = function () { 1838 | return (this.$arrow = this.$arrow || this.tip().find('.arrow')) 1839 | } 1840 | 1841 | 1842 | // POPOVER PLUGIN DEFINITION 1843 | // ========================= 1844 | 1845 | function Plugin(option) { 1846 | return this.each(function () { 1847 | var $this = $(this) 1848 | var data = $this.data('bs.popover') 1849 | var options = typeof option == 'object' && option 1850 | 1851 | if (!data && /destroy|hide/.test(option)) return 1852 | if (!data) $this.data('bs.popover', (data = new Popover(this, options))) 1853 | if (typeof option == 'string') data[option]() 1854 | }) 1855 | } 1856 | 1857 | var old = $.fn.popover 1858 | 1859 | $.fn.popover = Plugin 1860 | $.fn.popover.Constructor = Popover 1861 | 1862 | 1863 | // POPOVER NO CONFLICT 1864 | // =================== 1865 | 1866 | $.fn.popover.noConflict = function () { 1867 | $.fn.popover = old 1868 | return this 1869 | } 1870 | 1871 | }(jQuery); 1872 | 1873 | /* ======================================================================== 1874 | * Bootstrap: scrollspy.js v3.3.5 1875 | * http://getbootstrap.com/javascript/#scrollspy 1876 | * ======================================================================== 1877 | * Copyright 2011-2015 Twitter, Inc. 1878 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 1879 | * ======================================================================== */ 1880 | 1881 | 1882 | +function ($) { 1883 | 'use strict'; 1884 | 1885 | // SCROLLSPY CLASS DEFINITION 1886 | // ========================== 1887 | 1888 | function ScrollSpy(element, options) { 1889 | this.$body = $(document.body) 1890 | this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) 1891 | this.options = $.extend({}, ScrollSpy.DEFAULTS, options) 1892 | this.selector = (this.options.target || '') + ' .nav li > a' 1893 | this.offsets = [] 1894 | this.targets = [] 1895 | this.activeTarget = null 1896 | this.scrollHeight = 0 1897 | 1898 | this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) 1899 | this.refresh() 1900 | this.process() 1901 | } 1902 | 1903 | ScrollSpy.VERSION = '3.3.5' 1904 | 1905 | ScrollSpy.DEFAULTS = { 1906 | offset: 10 1907 | } 1908 | 1909 | ScrollSpy.prototype.getScrollHeight = function () { 1910 | return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) 1911 | } 1912 | 1913 | ScrollSpy.prototype.refresh = function () { 1914 | var that = this 1915 | var offsetMethod = 'offset' 1916 | var offsetBase = 0 1917 | 1918 | this.offsets = [] 1919 | this.targets = [] 1920 | this.scrollHeight = this.getScrollHeight() 1921 | 1922 | if (!$.isWindow(this.$scrollElement[0])) { 1923 | offsetMethod = 'position' 1924 | offsetBase = this.$scrollElement.scrollTop() 1925 | } 1926 | 1927 | this.$body 1928 | .find(this.selector) 1929 | .map(function () { 1930 | var $el = $(this) 1931 | var href = $el.data('target') || $el.attr('href') 1932 | var $href = /^#./.test(href) && $(href) 1933 | 1934 | return ($href 1935 | && $href.length 1936 | && $href.is(':visible') 1937 | && [[$href[offsetMethod]().top + offsetBase, href]]) || null 1938 | }) 1939 | .sort(function (a, b) { return a[0] - b[0] }) 1940 | .each(function () { 1941 | that.offsets.push(this[0]) 1942 | that.targets.push(this[1]) 1943 | }) 1944 | } 1945 | 1946 | ScrollSpy.prototype.process = function () { 1947 | var scrollTop = this.$scrollElement.scrollTop() + this.options.offset 1948 | var scrollHeight = this.getScrollHeight() 1949 | var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() 1950 | var offsets = this.offsets 1951 | var targets = this.targets 1952 | var activeTarget = this.activeTarget 1953 | var i 1954 | 1955 | if (this.scrollHeight != scrollHeight) { 1956 | this.refresh() 1957 | } 1958 | 1959 | if (scrollTop >= maxScroll) { 1960 | return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) 1961 | } 1962 | 1963 | if (activeTarget && scrollTop < offsets[0]) { 1964 | this.activeTarget = null 1965 | return this.clear() 1966 | } 1967 | 1968 | for (i = offsets.length; i--;) { 1969 | activeTarget != targets[i] 1970 | && scrollTop >= offsets[i] 1971 | && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) 1972 | && this.activate(targets[i]) 1973 | } 1974 | } 1975 | 1976 | ScrollSpy.prototype.activate = function (target) { 1977 | this.activeTarget = target 1978 | 1979 | this.clear() 1980 | 1981 | var selector = this.selector + 1982 | '[data-target="' + target + '"],' + 1983 | this.selector + '[href="' + target + '"]' 1984 | 1985 | var active = $(selector) 1986 | .parents('li') 1987 | .addClass('active') 1988 | 1989 | if (active.parent('.dropdown-menu').length) { 1990 | active = active 1991 | .closest('li.dropdown') 1992 | .addClass('active') 1993 | } 1994 | 1995 | active.trigger('activate.bs.scrollspy') 1996 | } 1997 | 1998 | ScrollSpy.prototype.clear = function () { 1999 | $(this.selector) 2000 | .parentsUntil(this.options.target, '.active') 2001 | .removeClass('active') 2002 | } 2003 | 2004 | 2005 | // SCROLLSPY PLUGIN DEFINITION 2006 | // =========================== 2007 | 2008 | function Plugin(option) { 2009 | return this.each(function () { 2010 | var $this = $(this) 2011 | var data = $this.data('bs.scrollspy') 2012 | var options = typeof option == 'object' && option 2013 | 2014 | if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) 2015 | if (typeof option == 'string') data[option]() 2016 | }) 2017 | } 2018 | 2019 | var old = $.fn.scrollspy 2020 | 2021 | $.fn.scrollspy = Plugin 2022 | $.fn.scrollspy.Constructor = ScrollSpy 2023 | 2024 | 2025 | // SCROLLSPY NO CONFLICT 2026 | // ===================== 2027 | 2028 | $.fn.scrollspy.noConflict = function () { 2029 | $.fn.scrollspy = old 2030 | return this 2031 | } 2032 | 2033 | 2034 | // SCROLLSPY DATA-API 2035 | // ================== 2036 | 2037 | $(window).on('load.bs.scrollspy.data-api', function () { 2038 | $('[data-spy="scroll"]').each(function () { 2039 | var $spy = $(this) 2040 | Plugin.call($spy, $spy.data()) 2041 | }) 2042 | }) 2043 | 2044 | }(jQuery); 2045 | 2046 | /* ======================================================================== 2047 | * Bootstrap: tab.js v3.3.5 2048 | * http://getbootstrap.com/javascript/#tabs 2049 | * ======================================================================== 2050 | * Copyright 2011-2015 Twitter, Inc. 2051 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 2052 | * ======================================================================== */ 2053 | 2054 | 2055 | +function ($) { 2056 | 'use strict'; 2057 | 2058 | // TAB CLASS DEFINITION 2059 | // ==================== 2060 | 2061 | var Tab = function (element) { 2062 | // jscs:disable requireDollarBeforejQueryAssignment 2063 | this.element = $(element) 2064 | // jscs:enable requireDollarBeforejQueryAssignment 2065 | } 2066 | 2067 | Tab.VERSION = '3.3.5' 2068 | 2069 | Tab.TRANSITION_DURATION = 150 2070 | 2071 | Tab.prototype.show = function () { 2072 | var $this = this.element 2073 | var $ul = $this.closest('ul:not(.dropdown-menu)') 2074 | var selector = $this.data('target') 2075 | 2076 | if (!selector) { 2077 | selector = $this.attr('href') 2078 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 2079 | } 2080 | 2081 | if ($this.parent('li').hasClass('active')) return 2082 | 2083 | var $previous = $ul.find('.active:last a') 2084 | var hideEvent = $.Event('hide.bs.tab', { 2085 | relatedTarget: $this[0] 2086 | }) 2087 | var showEvent = $.Event('show.bs.tab', { 2088 | relatedTarget: $previous[0] 2089 | }) 2090 | 2091 | $previous.trigger(hideEvent) 2092 | $this.trigger(showEvent) 2093 | 2094 | if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return 2095 | 2096 | var $target = $(selector) 2097 | 2098 | this.activate($this.closest('li'), $ul) 2099 | this.activate($target, $target.parent(), function () { 2100 | $previous.trigger({ 2101 | type: 'hidden.bs.tab', 2102 | relatedTarget: $this[0] 2103 | }) 2104 | $this.trigger({ 2105 | type: 'shown.bs.tab', 2106 | relatedTarget: $previous[0] 2107 | }) 2108 | }) 2109 | } 2110 | 2111 | Tab.prototype.activate = function (element, container, callback) { 2112 | var $active = container.find('> .active') 2113 | var transition = callback 2114 | && $.support.transition 2115 | && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) 2116 | 2117 | function next() { 2118 | $active 2119 | .removeClass('active') 2120 | .find('> .dropdown-menu > .active') 2121 | .removeClass('active') 2122 | .end() 2123 | .find('[data-toggle="tab"]') 2124 | .attr('aria-expanded', false) 2125 | 2126 | element 2127 | .addClass('active') 2128 | .find('[data-toggle="tab"]') 2129 | .attr('aria-expanded', true) 2130 | 2131 | if (transition) { 2132 | element[0].offsetWidth // reflow for transition 2133 | element.addClass('in') 2134 | } else { 2135 | element.removeClass('fade') 2136 | } 2137 | 2138 | if (element.parent('.dropdown-menu').length) { 2139 | element 2140 | .closest('li.dropdown') 2141 | .addClass('active') 2142 | .end() 2143 | .find('[data-toggle="tab"]') 2144 | .attr('aria-expanded', true) 2145 | } 2146 | 2147 | callback && callback() 2148 | } 2149 | 2150 | $active.length && transition ? 2151 | $active 2152 | .one('bsTransitionEnd', next) 2153 | .emulateTransitionEnd(Tab.TRANSITION_DURATION) : 2154 | next() 2155 | 2156 | $active.removeClass('in') 2157 | } 2158 | 2159 | 2160 | // TAB PLUGIN DEFINITION 2161 | // ===================== 2162 | 2163 | function Plugin(option) { 2164 | return this.each(function () { 2165 | var $this = $(this) 2166 | var data = $this.data('bs.tab') 2167 | 2168 | if (!data) $this.data('bs.tab', (data = new Tab(this))) 2169 | if (typeof option == 'string') data[option]() 2170 | }) 2171 | } 2172 | 2173 | var old = $.fn.tab 2174 | 2175 | $.fn.tab = Plugin 2176 | $.fn.tab.Constructor = Tab 2177 | 2178 | 2179 | // TAB NO CONFLICT 2180 | // =============== 2181 | 2182 | $.fn.tab.noConflict = function () { 2183 | $.fn.tab = old 2184 | return this 2185 | } 2186 | 2187 | 2188 | // TAB DATA-API 2189 | // ============ 2190 | 2191 | var clickHandler = function (e) { 2192 | e.preventDefault() 2193 | Plugin.call($(this), 'show') 2194 | } 2195 | 2196 | $(document) 2197 | .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) 2198 | .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) 2199 | 2200 | }(jQuery); 2201 | 2202 | /* ======================================================================== 2203 | * Bootstrap: affix.js v3.3.5 2204 | * http://getbootstrap.com/javascript/#affix 2205 | * ======================================================================== 2206 | * Copyright 2011-2015 Twitter, Inc. 2207 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 2208 | * ======================================================================== */ 2209 | 2210 | 2211 | +function ($) { 2212 | 'use strict'; 2213 | 2214 | // AFFIX CLASS DEFINITION 2215 | // ====================== 2216 | 2217 | var Affix = function (element, options) { 2218 | this.options = $.extend({}, Affix.DEFAULTS, options) 2219 | 2220 | this.$target = $(this.options.target) 2221 | .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) 2222 | .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) 2223 | 2224 | this.$element = $(element) 2225 | this.affixed = null 2226 | this.unpin = null 2227 | this.pinnedOffset = null 2228 | 2229 | this.checkPosition() 2230 | } 2231 | 2232 | Affix.VERSION = '3.3.5' 2233 | 2234 | Affix.RESET = 'affix affix-top affix-bottom' 2235 | 2236 | Affix.DEFAULTS = { 2237 | offset: 0, 2238 | target: window 2239 | } 2240 | 2241 | Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { 2242 | var scrollTop = this.$target.scrollTop() 2243 | var position = this.$element.offset() 2244 | var targetHeight = this.$target.height() 2245 | 2246 | if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false 2247 | 2248 | if (this.affixed == 'bottom') { 2249 | if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' 2250 | return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' 2251 | } 2252 | 2253 | var initializing = this.affixed == null 2254 | var colliderTop = initializing ? scrollTop : position.top 2255 | var colliderHeight = initializing ? targetHeight : height 2256 | 2257 | if (offsetTop != null && scrollTop <= offsetTop) return 'top' 2258 | if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' 2259 | 2260 | return false 2261 | } 2262 | 2263 | Affix.prototype.getPinnedOffset = function () { 2264 | if (this.pinnedOffset) return this.pinnedOffset 2265 | this.$element.removeClass(Affix.RESET).addClass('affix') 2266 | var scrollTop = this.$target.scrollTop() 2267 | var position = this.$element.offset() 2268 | return (this.pinnedOffset = position.top - scrollTop) 2269 | } 2270 | 2271 | Affix.prototype.checkPositionWithEventLoop = function () { 2272 | setTimeout($.proxy(this.checkPosition, this), 1) 2273 | } 2274 | 2275 | Affix.prototype.checkPosition = function () { 2276 | if (!this.$element.is(':visible')) return 2277 | 2278 | var height = this.$element.height() 2279 | var offset = this.options.offset 2280 | var offsetTop = offset.top 2281 | var offsetBottom = offset.bottom 2282 | var scrollHeight = Math.max($(document).height(), $(document.body).height()) 2283 | 2284 | if (typeof offset != 'object') offsetBottom = offsetTop = offset 2285 | if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) 2286 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) 2287 | 2288 | var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) 2289 | 2290 | if (this.affixed != affix) { 2291 | if (this.unpin != null) this.$element.css('top', '') 2292 | 2293 | var affixType = 'affix' + (affix ? '-' + affix : '') 2294 | var e = $.Event(affixType + '.bs.affix') 2295 | 2296 | this.$element.trigger(e) 2297 | 2298 | if (e.isDefaultPrevented()) return 2299 | 2300 | this.affixed = affix 2301 | this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null 2302 | 2303 | this.$element 2304 | .removeClass(Affix.RESET) 2305 | .addClass(affixType) 2306 | .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') 2307 | } 2308 | 2309 | if (affix == 'bottom') { 2310 | this.$element.offset({ 2311 | top: scrollHeight - height - offsetBottom 2312 | }) 2313 | } 2314 | } 2315 | 2316 | 2317 | // AFFIX PLUGIN DEFINITION 2318 | // ======================= 2319 | 2320 | function Plugin(option) { 2321 | return this.each(function () { 2322 | var $this = $(this) 2323 | var data = $this.data('bs.affix') 2324 | var options = typeof option == 'object' && option 2325 | 2326 | if (!data) $this.data('bs.affix', (data = new Affix(this, options))) 2327 | if (typeof option == 'string') data[option]() 2328 | }) 2329 | } 2330 | 2331 | var old = $.fn.affix 2332 | 2333 | $.fn.affix = Plugin 2334 | $.fn.affix.Constructor = Affix 2335 | 2336 | 2337 | // AFFIX NO CONFLICT 2338 | // ================= 2339 | 2340 | $.fn.affix.noConflict = function () { 2341 | $.fn.affix = old 2342 | return this 2343 | } 2344 | 2345 | 2346 | // AFFIX DATA-API 2347 | // ============== 2348 | 2349 | $(window).on('load', function () { 2350 | $('[data-spy="affix"]').each(function () { 2351 | var $spy = $(this) 2352 | var data = $spy.data() 2353 | 2354 | data.offset = data.offset || {} 2355 | 2356 | if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom 2357 | if (data.offsetTop != null) data.offset.top = data.offsetTop 2358 | 2359 | Plugin.call($spy, data) 2360 | }) 2361 | }) 2362 | 2363 | }(jQuery); 2364 | -------------------------------------------------------------------------------- /deployment/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.5",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.5",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.5",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.5",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.5",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.5",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.5",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.5",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /deployment/static/js/custom.js: -------------------------------------------------------------------------------- 1 | // Offset for Site Navigation 2 | $('#siteNav').affix({ 3 | offset: { 4 | top: 100 5 | } 6 | }) -------------------------------------------------------------------------------- /deployment/static/js/ie10-viewport-bug-workaround.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | // See the Getting Started docs for more information: 8 | // http://getbootstrap.com/getting-started/#support-ie10-width 9 | 10 | (function () { 11 | 'use strict'; 12 | 13 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) { 14 | var msViewportStyle = document.createElement('style') 15 | msViewportStyle.appendChild( 16 | document.createTextNode( 17 | '@-ms-viewport{width:auto!important}' 18 | ) 19 | ) 20 | document.querySelector('head').appendChild(msViewportStyle) 21 | } 22 | 23 | })(); 24 | -------------------------------------------------------------------------------- /deployment/static/js/jquery.easing.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ 3 | * 4 | * Uses the built in easing capabilities added In jQuery 1.1 5 | * to offer multiple easing options 6 | * 7 | * TERMS OF USE - EASING EQUATIONS 8 | * 9 | * Open source under the BSD License. 10 | * 11 | * Copyright © 2001 Robert Penner 12 | * All rights reserved. 13 | * 14 | * TERMS OF USE - jQuery Easing 15 | * 16 | * Open source under the BSD License. 17 | * 18 | * Copyright © 2008 George McGinley Smith 19 | * All rights reserved. 20 | * 21 | * Redistribution and use in source and binary forms, with or without modification, 22 | * are permitted provided that the following conditions are met: 23 | * 24 | * Redistributions of source code must retain the above copyright notice, this list of 25 | * conditions and the following disclaimer. 26 | * Redistributions in binary form must reproduce the above copyright notice, this list 27 | * of conditions and the following disclaimer in the documentation and/or other materials 28 | * provided with the distribution. 29 | * 30 | * Neither the name of the author nor the names of contributors may be used to endorse 31 | * or promote products derived from this software without specific prior written permission. 32 | * 33 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 34 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 35 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 36 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 37 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 38 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 39 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 40 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 41 | * OF THE POSSIBILITY OF SUCH DAMAGE. 42 | * 43 | */ 44 | jQuery.easing.jswing=jQuery.easing.swing;jQuery.extend(jQuery.easing,{def:"easeOutQuad",swing:function(e,f,a,h,g){return jQuery.easing[jQuery.easing.def](e,f,a,h,g)},easeInQuad:function(e,f,a,h,g){return h*(f/=g)*f+a},easeOutQuad:function(e,f,a,h,g){return -h*(f/=g)*(f-2)+a},easeInOutQuad:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f+a}return -h/2*((--f)*(f-2)-1)+a},easeInCubic:function(e,f,a,h,g){return h*(f/=g)*f*f+a},easeOutCubic:function(e,f,a,h,g){return h*((f=f/g-1)*f*f+1)+a},easeInOutCubic:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f+a}return h/2*((f-=2)*f*f+2)+a},easeInQuart:function(e,f,a,h,g){return h*(f/=g)*f*f*f+a},easeOutQuart:function(e,f,a,h,g){return -h*((f=f/g-1)*f*f*f-1)+a},easeInOutQuart:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f*f+a}return -h/2*((f-=2)*f*f*f-2)+a},easeInQuint:function(e,f,a,h,g){return h*(f/=g)*f*f*f*f+a},easeOutQuint:function(e,f,a,h,g){return h*((f=f/g-1)*f*f*f*f+1)+a},easeInOutQuint:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f*f*f+a}return h/2*((f-=2)*f*f*f*f+2)+a},easeInSine:function(e,f,a,h,g){return -h*Math.cos(f/g*(Math.PI/2))+h+a},easeOutSine:function(e,f,a,h,g){return h*Math.sin(f/g*(Math.PI/2))+a},easeInOutSine:function(e,f,a,h,g){return -h/2*(Math.cos(Math.PI*f/g)-1)+a},easeInExpo:function(e,f,a,h,g){return(f==0)?a:h*Math.pow(2,10*(f/g-1))+a},easeOutExpo:function(e,f,a,h,g){return(f==g)?a+h:h*(-Math.pow(2,-10*f/g)+1)+a},easeInOutExpo:function(e,f,a,h,g){if(f==0){return a}if(f==g){return a+h}if((f/=g/2)<1){return h/2*Math.pow(2,10*(f-1))+a}return h/2*(-Math.pow(2,-10*--f)+2)+a},easeInCirc:function(e,f,a,h,g){return -h*(Math.sqrt(1-(f/=g)*f)-1)+a},easeOutCirc:function(e,f,a,h,g){return h*Math.sqrt(1-(f=f/g-1)*f)+a},easeInOutCirc:function(e,f,a,h,g){if((f/=g/2)<1){return -h/2*(Math.sqrt(1-f*f)-1)+a}return h/2*(Math.sqrt(1-(f-=2)*f)+1)+a},easeInElastic:function(f,h,e,l,k){var i=1.70158;var j=0;var g=l;if(h==0){return e}if((h/=k)==1){return e+l}if(!j){j=k*0.3}if(g 2 | 3 | 4 | 5 | GPT-2 Gen 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 |

15 | Simple Text-Gen w/ OpenAI GPT-2 16 |

17 | 18 | {% block content %} 19 | {% for message in form.seed.errors %} 20 |
{{ message }}
21 | {% endfor %} 22 | 23 |
24 | 25 | {{ form.seed.label }} 26 | {{ form.seed }} 27 | 28 | {{ form.submit }} 29 |
30 | {% endblock %} 31 | 32 |
33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /deployment/templates/seeded.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | Generated Text 6 | 7 | 10 |
11 | 12 | 13 |
14 | {% block content %} 15 | {{ input|safe }} 16 | {% endblock %} 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /deployment/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def header(text, color='black', gen_text=None): 5 | """Create an HTML header""" 6 | 7 | if gen_text: 8 | raw_html = f'

' + str( 9 | text) + '' + str(gen_text) + '

' 10 | else: 11 | raw_html = f'

' + str( 12 | text) + '

' 13 | return raw_html 14 | 15 | 16 | def box(text, gen_text=None): 17 | """Create an HTML box of text""" 18 | 19 | if gen_text: 20 | raw_html = '
' + str( 21 | text) + '' + str(gen_text) + '
' 22 | 23 | else: 24 | raw_html = '
' + str( 25 | text) + '
' 26 | return raw_html 27 | 28 | 29 | def add_content(old_html, raw_html): 30 | """Add html content together""" 31 | 32 | old_html += raw_html 33 | return old_html 34 | 35 | 36 | def format_sequence(s): 37 | """Add spaces around punctuation and remove references to images/citations.""" 38 | 39 | # Add spaces around punctuation 40 | s = re.sub(r'(?<=[^\s0-9])(?=[.,;?])', r' ', s) 41 | 42 | # Remove references to figures 43 | s = re.sub(r'\((\d+)\)', r'', s) 44 | 45 | # Remove double spaces 46 | s = re.sub(r'\s\s', ' ', s) 47 | return s 48 | 49 | 50 | def remove_spaces(s): 51 | """Remove spaces around punctuation""" 52 | 53 | s = re.sub(r'\s+([.,;?])', r'\1', s) 54 | 55 | return s 56 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | flask: 4 | image: gpt-2-flask-api 5 | build: . 6 | command: /opt/conda/envs/ml-flask/bin/python run_server.py 7 | ports: 8 | - "5000:5000" 9 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - python==3.7 3 | - pip: 4 | - Flask==1.0.2 5 | - torch==1.0.1 6 | - regex==2017.4.5 7 | - requests==2.21.0 8 | - numpy==1.16.2 9 | - wtforms==2.2.1 10 | - tqdm==4.31.1 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.2 2 | torch==1.0.1 3 | regex==2017.4.5 4 | requests==2.21.0 5 | numpy==1.16.2 6 | wtforms==2.2.1 7 | tqdm==4.31.1 8 | --------------------------------------------------------------------------------