├── server ├── __init__.py ├── requirements.txt ├── index.html ├── main.py ├── mary.py └── vision.py ├── .gitignore ├── static ├── audio │ ├── crash.mp3 │ ├── end.mp3 │ ├── fill0.mp3 │ ├── fill1.mp3 │ ├── fill2.mp3 │ ├── racer.mp3 │ ├── siren.mp3 │ ├── shutter.mp3 │ ├── applause.mp3 │ ├── voice │ │ ├── yeah.mp3 │ │ ├── yeahup.mp3 │ │ ├── alright.mp3 │ │ ├── herewego.mp3 │ │ ├── herewegogo.mp3 │ │ ├── readytoexplore.mp3 │ │ └── takeapicture.mp3 │ └── reverseCrash.mp3 ├── images │ ├── camera.png │ ├── rotate.png │ ├── shutter.png │ ├── x_button.png │ ├── giorgio_left.png │ ├── giorgio_right.png │ ├── face │ │ ├── giorgio_logo_anim__0000_eyes_right.png │ │ ├── giorgio_logo_anim__0002_mouth_open.png │ │ ├── giorgio_logo_anim__0009_big_notes_1.png │ │ ├── giorgio_logo_anim__0010_big_notes_2.png │ │ ├── giorgio_logo_anim__0011_big_notes_3.png │ │ ├── giorgio_logo_anim__0012_big_notes_4.png │ │ ├── giorgio_logo_anim__0013_big_notes_5.png │ │ ├── giorgio_logo_anim__0003_little_note_1.png │ │ ├── giorgio_logo_anim__0004_little_note_2.png │ │ ├── giorgio_logo_anim__0005_little_note_3.png │ │ ├── giorgio_logo_anim__0006_little_note_4.png │ │ ├── giorgio_logo_anim__0007_little_note_5.png │ │ ├── giorgio_logo_anim__0008_little_note_6.png │ │ └── giorgio_logo_anim__0001_eyes_right_blink.png │ ├── camera_icon.svg │ ├── play_button.svg │ ├── badgeAI_master.svg │ └── badgeFriends_master.svg ├── style │ ├── pixel_operator │ │ ├── PixelOperator.ttf │ │ ├── PixelOperator8.ttf │ │ ├── PixelOperatorHB.ttf │ │ ├── PixelOperatorSC.ttf │ │ ├── PixelOperatorHB8.ttf │ │ ├── PixelOperatorHBSC.ttf │ │ ├── PixelOperatorMono.ttf │ │ ├── PixelOperator-Bold.ttf │ │ ├── PixelOperator8-Bold.ttf │ │ ├── PixelOperatorMono8.ttf │ │ ├── PixelOperatorMonoHB.ttf │ │ ├── PixelOperatorMonoHB8.ttf │ │ ├── PixelOperatorSC-Bold.ttf │ │ ├── PixelOperatorMono-Bold.ttf │ │ ├── PixelOperatorMono8-Bold.ttf │ │ └── LICENSE.txt │ ├── notsupported.css │ ├── close.css │ ├── title.css │ ├── lines.css │ ├── common.css │ ├── nocamera.css │ ├── main.css │ ├── text.css │ ├── shutter.css │ ├── camera.css │ ├── about.css │ └── splash.css ├── index.html ├── src │ ├── Visible.js │ ├── Config.js │ ├── music │ │ ├── Timeline.js │ │ ├── Fill.js │ │ ├── Music.js │ │ ├── Position.js │ │ └── Background.js │ ├── interface │ │ ├── NoCamera.js │ │ ├── Loader.js │ │ ├── Orientation.js │ │ ├── Close.js │ │ ├── Shutter.js │ │ ├── Splash.js │ │ ├── About.js │ │ └── Title.js │ ├── FeatureTest.js │ ├── voice │ │ ├── Intro.js │ │ ├── Lines.js │ │ ├── SpeakBuffer.js │ │ ├── Filler.js │ │ ├── Lyrics.js │ │ ├── VoiceEffects.js │ │ ├── Wait.js │ │ ├── Voice.js │ │ └── Text.js │ ├── camera │ │ ├── Label.js │ │ ├── Camera.js │ │ ├── Image.js │ │ └── Canvas.js │ └── Main.js ├── package.json ├── webpack.config.js └── third_party │ └── Modernizr.js ├── app.yaml ├── CONTRIBUTING.md └── README.md /server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | google-api-python-client==1.5.1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | server/data/key.json 2 | node_modules/ 3 | .DS_Store 4 | *.pyc 5 | static/build/* 6 | static/images/font/ -------------------------------------------------------------------------------- /static/audio/crash.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/crash.mp3 -------------------------------------------------------------------------------- /static/audio/end.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/end.mp3 -------------------------------------------------------------------------------- /static/audio/fill0.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/fill0.mp3 -------------------------------------------------------------------------------- /static/audio/fill1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/fill1.mp3 -------------------------------------------------------------------------------- /static/audio/fill2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/fill2.mp3 -------------------------------------------------------------------------------- /static/audio/racer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/racer.mp3 -------------------------------------------------------------------------------- /static/audio/siren.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/siren.mp3 -------------------------------------------------------------------------------- /static/audio/shutter.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/shutter.mp3 -------------------------------------------------------------------------------- /static/images/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/camera.png -------------------------------------------------------------------------------- /static/images/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/rotate.png -------------------------------------------------------------------------------- /static/audio/applause.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/applause.mp3 -------------------------------------------------------------------------------- /static/audio/voice/yeah.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/voice/yeah.mp3 -------------------------------------------------------------------------------- /static/images/shutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/shutter.png -------------------------------------------------------------------------------- /static/images/x_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/x_button.png -------------------------------------------------------------------------------- /static/audio/reverseCrash.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/reverseCrash.mp3 -------------------------------------------------------------------------------- /static/audio/voice/yeahup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/voice/yeahup.mp3 -------------------------------------------------------------------------------- /static/audio/voice/alright.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/voice/alright.mp3 -------------------------------------------------------------------------------- /static/audio/voice/herewego.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/voice/herewego.mp3 -------------------------------------------------------------------------------- /static/images/giorgio_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/giorgio_left.png -------------------------------------------------------------------------------- /static/images/giorgio_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/giorgio_right.png -------------------------------------------------------------------------------- /static/audio/voice/herewegogo.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/voice/herewegogo.mp3 -------------------------------------------------------------------------------- /static/audio/voice/readytoexplore.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/voice/readytoexplore.mp3 -------------------------------------------------------------------------------- /static/audio/voice/takeapicture.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/audio/voice/takeapicture.mp3 -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperator.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperator.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperator8.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperator8.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorHB.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorHB.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorSC.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorSC.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorHB8.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorHB8.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorHBSC.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorHBSC.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorMono.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperator-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperator-Bold.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperator8-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperator8-Bold.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorMono8.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorMono8.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorMonoHB.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorMonoHB.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorMonoHB8.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorMonoHB8.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorSC-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorSC-Bold.ttf -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorMono-Bold.ttf -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0000_eyes_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0000_eyes_right.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0002_mouth_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0002_mouth_open.png -------------------------------------------------------------------------------- /static/style/pixel_operator/PixelOperatorMono8-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/style/pixel_operator/PixelOperatorMono8-Bold.ttf -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0009_big_notes_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0009_big_notes_1.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0010_big_notes_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0010_big_notes_2.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0011_big_notes_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0011_big_notes_3.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0012_big_notes_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0012_big_notes_4.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0013_big_notes_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0013_big_notes_5.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0003_little_note_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0003_little_note_1.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0004_little_note_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0004_little_note_2.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0005_little_note_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0005_little_note_3.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0006_little_note_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0006_little_note_4.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0007_little_note_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0007_little_note_5.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0008_little_note_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0008_little_note_6.png -------------------------------------------------------------------------------- /static/images/face/giorgio_logo_anim__0001_eyes_right_blink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/aiexperiments-giorgio-cam/HEAD/static/images/face/giorgio_logo_anim__0001_eyes_right_blink.png -------------------------------------------------------------------------------- /server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /static/images/camera_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /static/src/Visible.js: -------------------------------------------------------------------------------- 1 | import Visibility from 'visibilityjs' 2 | 3 | export default function resolveIfVisible(response){ 4 | if (Visibility.hidden()){ 5 | return new Promise((success) => { 6 | const id = Visibility.onVisible(() => { 7 | //and a short pause to give the 8 | //clock a chance to catch up 9 | setTimeout(() => { 10 | Visibility.unbind(id) 11 | success(response) 12 | }, 500) 13 | }) 14 | }) 15 | } else { 16 | return Promise.resolve(response) 17 | } 18 | } -------------------------------------------------------------------------------- /static/images/play_button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /static/style/notsupported.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | #notsupported { 18 | position: relative; 19 | margin: 200px auto 0px auto; 20 | width: 80%; 21 | min-width: 300px; 22 | font-size: 40px; 23 | line-height: 50px; 24 | text-align: center; 25 | color: white; 26 | 27 | a { 28 | color: white; 29 | text-decoration: underline; 30 | cursor: pointer; 31 | } 32 | } -------------------------------------------------------------------------------- /server/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2016 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import os 19 | import webapp2 20 | from google.appengine.ext.webapp import template 21 | 22 | 23 | class Main(webapp2.RequestHandler): 24 | 25 | def get(self): 26 | main = os.path.join(os.path.dirname(__file__), 'index.html') 27 | self.response.out.write(template.render(main, {})) 28 | 29 | 30 | app = webapp2.WSGIApplication([ 31 | ('/', Main) 32 | ], debug=True) 33 | -------------------------------------------------------------------------------- /static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "giorgio-cam", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Yotam Mann", 10 | "dependencies": { 11 | "autoprefixer": "^6.3.7", 12 | "babel-core": "^6.11.4", 13 | "babel-loader": "^6.2.4", 14 | "babel-preset-es2015": "^6.9.0", 15 | "css-loader": "^0.23.1", 16 | "domready": "^1.0.8", 17 | "exports-loader": "^0.6.3", 18 | "file-loader": "^0.9.0", 19 | "is-mobile": "^0.2.2", 20 | "knuth-shuffle": "^1.0.1", 21 | "postcss-calc": "^5.3.0", 22 | "postcss-loader": "^0.9.1", 23 | "precss": "^1.4.0", 24 | "screenfull": "^3.0.2", 25 | "startaudiocontext": "^1.2.0", 26 | "style-loader": "^0.13.1", 27 | "three": "^0.81.0", 28 | "tone": "^0.8.0", 29 | "tween.js": "^16.3.5", 30 | "url-loader": "^0.5.7", 31 | "visibilityjs": "^1.2.4", 32 | "webpack": "^1.13.1", 33 | "webrtc-adapter": "^2.0.3", 34 | "youtube-iframe": "^1.0.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /static/src/Config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Transport from 'Tone/core/Transport' 18 | import Time from 'Tone/type/Time' 19 | 20 | Transport.bpm.value = 128 21 | 22 | export default { 23 | startingPosition: 0, 24 | cloudVision : true, 25 | quantizeLevel : '4n', 26 | name : 'Giorgio Cam', 27 | intro : true, 28 | aiExperimentsLink : 'https://aiexperiments.withgoogle.com', 29 | //mock throws a quota limit error 30 | quotaLimit : false, 31 | fallbackScreen : false, 32 | fullscreen : false || window.location.search.indexOf('fullscreen') !== -1, 33 | fillTimeline : '138m', 34 | fadeOutTime : Time('4m').toSeconds() 35 | } -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: true 4 | 5 | 6 | handlers: 7 | 8 | - url: / 9 | script: server.main.app 10 | secure: always 11 | 12 | - url: /speak/.* 13 | script: server.mary.app 14 | 15 | - url: /see 16 | script: server.vision.app 17 | 18 | - url: /(.*\.js) 19 | mime_type: text/javascript 20 | static_files: static/\1 21 | upload: static/(.*\.js) 22 | 23 | - url: /(.*\.html) 24 | mime_type: text/html 25 | static_files: static/\1 26 | upload: static/(.*\.html) 27 | 28 | - url: /(.*\.(bmp|gif|ico|jpeg|jpg|png|svg|ttf)) 29 | static_files: static/\1 30 | upload: static/(.*\.(bmp|gif|ico|jpeg|jpg|png|svg|ttf)) 31 | 32 | # index files 33 | - url: /(.+)/ 34 | static_files: static/\1/index.html 35 | upload: static/(.+)/index.html 36 | 37 | # audio files 38 | - url: /(.*\.mp3) 39 | mime_type: audio/mpeg 40 | static_files: static/\1 41 | upload: static/audio/(.*\.mp3) 42 | 43 | 44 | skip_files: 45 | - ^(.*/)?.*\.pyc 46 | - ^(.*/)?.*\.wav 47 | - ^.git/.* 48 | - ^(.*/)?node_modules/.* 49 | - ^static/src/.* 50 | - ^static/style/.* 51 | - ^package\.json 52 | 53 | libraries: 54 | - name: webapp2 55 | version: latest 56 | 57 | env_variables: 58 | GOOGLE_APPLICATION_CREDENTIALS: PATH/TO/CLOUD_VISION_KEY.json -------------------------------------------------------------------------------- /static/style/close.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | @import 'common.css'; 18 | 19 | #closeButton { 20 | @mixin closeButton; 21 | } 22 | 23 | #end { 24 | position: absolute; 25 | top: 0px; 26 | left: 0px; 27 | width: 100%; 28 | height: 100%; 29 | z-index: 100002; 30 | font-weight: bold; 31 | color: white; 32 | 33 | a { 34 | @mixin yellowLink; 35 | } 36 | 37 | #text { 38 | position: absolute; 39 | width: 100%; 40 | line-height: 60px; 41 | font-size: 32px; 42 | top: 50%; 43 | transform: translate(0%, -50%); 44 | letter-spacing: 1px; 45 | text-align: center; 46 | 47 | #clickHere { 48 | @mixin yellowLink; 49 | } 50 | 51 | } 52 | } -------------------------------------------------------------------------------- /static/style/title.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | #title { 18 | height: 33px; 19 | position: relative; 20 | width: auto; 21 | display: inline-block; 22 | 23 | * { 24 | display: inline-block; 25 | float: left; 26 | height: 100%; 27 | background-size: auto 100%; 28 | background-repeat: no-repeat; 29 | } 30 | 31 | #giorgi { 32 | width: 146px; 33 | background-image: url(../images/giorgio_left.png); 34 | } 35 | 36 | #face { 37 | width: 57px; 38 | background-image: url(../images/face/giorgio_logo_anim__0000_eyes_right.png); 39 | 40 | img { 41 | width: 100%; 42 | height: 100%; 43 | } 44 | } 45 | 46 | #cam { 47 | width: 84px; 48 | background-image: url(../images/giorgio_right.png); 49 | } 50 | } -------------------------------------------------------------------------------- /static/src/music/Timeline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Transport from 'Tone/core/Transport' 18 | import Loop from 'Tone/event/Loop' 19 | import Background from 'music/Background' 20 | import Time from 'Tone/type/Time' 21 | import Fill from 'music/Fill' 22 | import Config from 'Config' 23 | import Master from 'Tone/core/Master' 24 | 25 | export default class Timeline{ 26 | constructor(){ 27 | 28 | 29 | } 30 | 31 | start(){ 32 | let startTime = Transport.now() + 0.7 33 | Transport.start(startTime, Config.intro ? 0 : '2m') 34 | Transport.loopStart = '10m' 35 | Transport.loopEnd = '18m' 36 | Transport.loop = true; 37 | return startTime 38 | } 39 | 40 | stop(){ 41 | Transport.stop() 42 | Transport.clear(0) 43 | } 44 | 45 | next(){ 46 | 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /static/style/lines.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | #textContainer { 18 | position: absolute; 19 | top: 0px; 20 | left: 0px; 21 | width: 100%; 22 | height: 100%; 23 | background-color: rgba(0, 0, 0, 0.2); 24 | pointer-events: none; 25 | 26 | #textLine { 27 | position: absolute; 28 | width: calc(100% - 40px); 29 | max-width: 500px; 30 | text-align: center; 31 | color: white; 32 | pointer-events: none; 33 | font-size: 46px; 34 | top: 50%; 35 | left: 50%; 36 | transform: translate(-50%, -50%); 37 | 38 | span { 39 | margin-right: 10px; 40 | word-wrap: normal; 41 | display: inline-block; 42 | opacity: 0; 43 | 44 | &.label { 45 | text-decoration: underline; 46 | } 47 | 48 | &.visible { 49 | opacity: 1; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /static/style/common.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | $closeSize : 25px; 18 | 19 | @define-mixin closeButton { 20 | position: absolute; 21 | top: 20px; 22 | right: 20px; 23 | width: $closeSize; 24 | height: $closeSize; 25 | background-image: url(../images/x_button.png); 26 | background-size: 100% 100%; 27 | color: white; 28 | pointer-events: none; 29 | opacity: 0; 30 | cursor: pointer; 31 | z-index: 100001; 32 | transition: transform 0.2s; 33 | -webkit-tap-highlight-color: rgba(0,0,0,0); 34 | 35 | &.visible { 36 | opacity: 1; 37 | pointer-events: initial; 38 | 39 | &:hover { 40 | transform: scale(1.1); 41 | 42 | &:active { 43 | opacity: 0.4; 44 | } 45 | } 46 | 47 | } 48 | } 49 | 50 | @define-mixin yellowLink { 51 | color: rgb(251, 240, 57); 52 | text-decoration: underline; 53 | cursor: pointer; 54 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at the end). 2 | 3 | ### Before you contribute 4 | Before we can use your code, you must sign the 5 | [Google Individual Contributor License Agreement] 6 | (https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | Before you start working on a larger contribution, you should get in touch with 15 | us first through the issue tracker with your idea so that we can help out and 16 | possibly guide you. Coordinating up front makes it much easier to avoid 17 | frustration later on. 18 | 19 | ### Code reviews 20 | All submissions, including submissions by project members, require review. We 21 | use Github pull requests for this purpose. 22 | 23 | ### The small print 24 | Contributions made by corporations are covered by a different agreement than 25 | the one above, the 26 | [Software Grant and Corporate Contributor License Agreement] 27 | (https://cla.developers.google.com/about/google-corporate). -------------------------------------------------------------------------------- /static/style/nocamera.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | @import 'common.css'; 18 | 19 | 20 | #nocamera { 21 | width: 100%; 22 | height: 100%; 23 | position: absolute; 24 | top: 0px; 25 | left: 0px; 26 | background-color: black; 27 | z-index: 10000; 28 | 29 | #content { 30 | position: absolute; 31 | top: 50%; 32 | left: 50%; 33 | width: 205px; 34 | transform: translate(-50%, -50%); 35 | text-align: center; 36 | font-family: 'Pixel Operator', monospace; 37 | 38 | * { 39 | position: relative; 40 | margin-top: 20px; 41 | font-size: 20px; 42 | color: white; 43 | } 44 | 45 | $iconSize : 60px; 46 | 47 | #camera { 48 | background-image: url(../images/camera.png); 49 | background-size: 100% 100%; 50 | width: $iconSize; 51 | height: $iconSize; 52 | margin: 10px auto 30px auto; 53 | } 54 | 55 | #blurb { 56 | text-transform: none; 57 | } 58 | 59 | #restart { 60 | @mixin yellowLink; 61 | } 62 | 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /static/src/interface/NoCamera.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import 'style/nocamera.css' 18 | 19 | export default function NoCamera(container){ 20 | 21 | const background = document.createElement('div') 22 | background.id = 'nocamera' 23 | container.appendChild(background) 24 | 25 | const content = document.createElement('div') 26 | content.id = 'content' 27 | background.appendChild(content) 28 | 29 | const img = document.createElement('div') 30 | img.id = 'camera' 31 | content.appendChild(img) 32 | 33 | const blurb = document.createElement('div') 34 | blurb.id = 'blurb' 35 | blurb.textContent = 'Since you didn\'t allow access to the camera, this experiment won\'t work.' 36 | content.appendChild(blurb) 37 | 38 | /*const restartButton = document.createElement('div') 39 | restartButton.id = 'restart' 40 | restartButton.textContent = 'Please restart' 41 | content.appendChild(restartButton) 42 | restartButton.addEventListener('click', () => { 43 | window.location.reload() 44 | })*/ 45 | } -------------------------------------------------------------------------------- /static/style/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | @font-face{ 18 | font-family: 'Pixel Operator Bold'; 19 | src : url(pixel_operator/PixelOperator-Bold.ttf); 20 | } 21 | 22 | @font-face{ 23 | font-family: 'Pixel Operator'; 24 | src : url(pixel_operator/PixelOperator.ttf); 25 | } 26 | 27 | body { 28 | font-family: 'Pixel Operator Bold', monospace; 29 | position: absolute; 30 | top: 0px; 31 | left: 0px; 32 | width: 100%; 33 | height: 100%; 34 | margin: 0px; 35 | background-size: 100% 100%; 36 | background-color: black; 37 | color: white; 38 | overflow: hidden; 39 | text-transform: uppercase; 40 | 41 | *:focus { 42 | outline: none; 43 | user-select: none; 44 | } 45 | 46 | #orientationBlocker { 47 | position: absolute; 48 | z-index: 1000000000; 49 | top: 0px; 50 | left: 0px; 51 | width: 100%; 52 | height: 100%; 53 | background-color: black; 54 | background-image: url(../images/rotate.png); 55 | background-repeat: no-repeat; 56 | background-position: center center; 57 | background-size: 100% auto; 58 | } 59 | } -------------------------------------------------------------------------------- /static/src/FeatureTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import 'style/main.css' 18 | import domReady from 'domready' 19 | import 'style/splash.css' 20 | import Modernizr from 'exports?Modernizr!Modernizr' 21 | 22 | require.ensure(['Main', 'style/notsupported.css'], (require) => { 23 | 24 | domReady(() => { 25 | 26 | if (Modernizr.getusermedia && Modernizr.webaudio && Modernizr.webgl){ 27 | 28 | const main = require('Main') 29 | 30 | } else { 31 | 32 | require('style/notsupported.css') 33 | 34 | const text = document.createElement('div') 35 | text.id = 'notsupported' 36 | if (!Modernizr.getusermedia){ 37 | text.innerHTML = 'Sorry, your device doesn’t allow access to the camera. Try using Chrome on an Android phone or a laptop.' 38 | } else { 39 | text.innerHTML = 'Oops, sorry for the tech trouble. For the best experience view in Chrome Browser' 40 | } 41 | document.body.appendChild(text) 42 | 43 | } 44 | 45 | }) 46 | 47 | }) 48 | 49 | 50 | -------------------------------------------------------------------------------- /static/src/music/Fill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Player from 'Tone/source/Player' 18 | import Transport from 'Tone/core/Transport' 19 | import Config from 'Config' 20 | import MusicPosition from 'music/Position' 21 | 22 | export default class Fill{ 23 | constructor(){ 24 | this._beats = { 25 | 0 : new Player('./audio/fill0.mp3').toMaster(), 26 | 1 : new Player({ url : './audio/fill1.mp3', volume : -2}).toMaster(), 27 | 2 : new Player({ url : './audio/fill2.mp3', volume : -3}).toMaster(), 28 | } 29 | this._currentPlaying = this._beats[0] 30 | 31 | //loop all of them 32 | for (let b in this._beats){ 33 | this._beats[b].loop = true 34 | this._beats[b].loopStart = '5m' 35 | this._beats[b].loopEnd = '6m' 36 | } 37 | 38 | this._crash = new Player('./audio/crash.mp3').toMaster() 39 | } 40 | 41 | fill(){ 42 | this._currentPlaying = this._beats[MusicPosition.fillPosition] 43 | this._currentPlaying.start(`@${Config.quantizeLevel}`, MusicPosition.end ? '2m' : 0) 44 | } 45 | 46 | fillEnd(){ 47 | this._currentPlaying.stop(`@${Config.quantizeLevel}`) 48 | this._crash.start(`@${Config.quantizeLevel} - 8n`) 49 | } 50 | 51 | stop(){ 52 | this._currentPlaying.stop() 53 | this._crash.stop() 54 | } 55 | } -------------------------------------------------------------------------------- /static/src/interface/Loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Buffer from 'Tone/core/Buffer' 18 | import Tone from 'Tone/core/Tone' 19 | import events from 'events' 20 | const EventEmitter = events.EventEmitter 21 | import StartAudioContext from 'startaudiocontext' 22 | 23 | export default class Loader extends EventEmitter{ 24 | constructor(container){ 25 | super() 26 | 27 | const loader = document.createElement('div') 28 | loader.id = 'loader' 29 | container.appendChild(loader) 30 | 31 | const loaderText = document.createElement('div') 32 | loaderText.id = 'loaderText' 33 | loaderText.textContent = 'loading' 34 | loader.appendChild(loaderText) 35 | 36 | const fill = document.createElement('div') 37 | fill.id = 'fill' 38 | loader.appendChild(fill) 39 | 40 | const fillText = document.createElement('div') 41 | fillText.id = 'fillText' 42 | fillText.textContent = 'loading' 43 | fill.appendChild(fillText) 44 | 45 | StartAudioContext(Tone.context, loader) 46 | 47 | Buffer.on('load', () => { 48 | 49 | fillText.textContent = 'Let\'s go' 50 | loader.classList.add('clickable') 51 | 52 | loader.addEventListener('click', () => { 53 | 54 | this.emit('start') 55 | }) 56 | }) 57 | 58 | Buffer.on('progress', (prog) => { 59 | 60 | fill.style.width = `${(prog * 100).toFixed(2)}%` 61 | 62 | }) 63 | } 64 | } -------------------------------------------------------------------------------- /static/src/voice/Intro.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import SpeakBuffer from 'voice/SpeakBuffer' 18 | import Time from 'Tone/type/Time' 19 | import Lines from 'voice/Lines' 20 | import Config from 'Config' 21 | import AudioBuffers from 'Tone/core/Buffers' 22 | 23 | export default class Intro { 24 | 25 | constructor(){ 26 | this._didIntro = false 27 | 28 | this._buffers = ['Ready to rock image recognition with me?', 'Take a picture and I\'ll tell you what i see'] 29 | this._buffers.forEach((text, index) => { 30 | SpeakBuffer.getBuffer(text, 1.25, -1).then((buffer) => { 31 | this._buffers[index] = {text, buffer} 32 | }) 33 | }) 34 | 35 | /*this._buffers = new AudioBuffers(['readytoexplore.mp3', 'takeapicture.mp3'], undefined, 'audio/voice/') 36 | this._text = ['Ready to explore Image Recognition with me?', 'Take a picture and I\'ll tell you what I see.']*/ 37 | } 38 | 39 | start(time){ 40 | if (Config.intro && !this._didIntro){ 41 | this._didIntro = true 42 | const lines = Lines.intro 43 | // SpeakBuffer.play(this._buffers[1].text, this._buffers[1].buffer, Time(time).add('2n'), 'dry') 44 | SpeakBuffer.play(this._buffers[0].text, this._buffers[0].buffer, Time(time), 'dry') 45 | return SpeakBuffer.play(this._buffers[1].text, this._buffers[1].buffer, Time(time).add('1m'), 'dry') 46 | } else { 47 | return Promise.resolve() 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /static/style/text.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | #textContainer { 18 | position: absolute; 19 | top: 0px; 20 | left: 0px; 21 | width: 100%; 22 | height: 100%; 23 | z-index: 10000; 24 | background-color: rgba(0, 0, 0, 0.3); 25 | pointer-events: none; 26 | 27 | 28 | .utterance { 29 | 30 | width: 80%; 31 | min-width: 300px; 32 | text-align: center; 33 | position: absolute; 34 | top: 50%; 35 | left: 50%; 36 | transform: translate(-50%, -50%); 37 | 38 | &.shiftUp { 39 | top: 45%; 40 | } 41 | 42 | span { 43 | margin: 0 10px 0 10px; 44 | visibility: hidden; 45 | display: inline-block; 46 | font-size: 50px; 47 | line-height: 70px; 48 | 49 | &.visible { 50 | visibility: initial; 51 | } 52 | 53 | $labelMargin: 20px; 54 | 55 | &.label { 56 | 57 | 58 | display: block; 59 | margin: $labelMargin 0px $labelMargin 0px; 60 | 61 | $spanMargin : 3px; 62 | 63 | span { 64 | margin: $spanMargin $spanMargin $spanMargin; 65 | line-height: calc(70px - $spanMargin * 2); 66 | padding: 10px; 67 | background-color: black; 68 | } 69 | 70 | div.score { 71 | visibility: hidden; 72 | width: 100%; 73 | font-size: 18px; 74 | line-height: 26px; 75 | text-align: center; 76 | text-transform: uppercase; 77 | 78 | &.visible { 79 | visibility: initial; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /static/src/camera/Label.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Config from 'Config' 18 | 19 | export default function Label(canvas){ 20 | 21 | if (Config.quotaLimit){ 22 | return new Promise((success, fail) => { 23 | setTimeout(() => { 24 | fail('quota') 25 | }, 2000) 26 | }) 27 | } else if (Config.cloudVision){ 28 | let formData = new FormData(); 29 | formData.append('image', canvas.toDataURL('image/png').replace(/^data:image\/(png|jpg);base64,/, '')); 30 | 31 | 32 | return new Promise((success, fail) => { 33 | let request = new XMLHttpRequest(); 34 | request.timeout = 20000 35 | request.open('POST', '/see'); 36 | request.addEventListener('load', () => { 37 | if (request.status == 200) { 38 | const response = JSON.parse(request.response) 39 | if (Array.isArray(response)){ 40 | success(JSON.parse(request.response)) 41 | } else if (response.error){ 42 | fail(response.error) 43 | } 44 | } else { 45 | fail(request.status) 46 | } 47 | }) 48 | request.addEventListener('error', fail) 49 | request.addEventListener('timeout', fail) 50 | request.send(formData) 51 | }) 52 | } else { 53 | return new Promise((success) => { 54 | setTimeout(() => { 55 | success([{ 56 | label : 'this is a very long multi word label', 57 | score : 0.8 58 | }, { 59 | label : 'this is another very long multi word label multi word', 60 | score : 0.8 61 | }]) 62 | }, 200) 63 | }) 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /static/src/interface/Orientation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Master from 'Tone/core/Master' 18 | 19 | const orientationBlocker = document.createElement('orientationBlocker') 20 | orientationBlocker.id = 'orientationBlocker' 21 | let hasBlocker = false 22 | 23 | function isLandscape(angle){ 24 | const landscape = Math.abs(angle) === 90 || Math.abs(angle) === 270 25 | if (landscape){ 26 | if (!hasBlocker){ 27 | hasBlocker = true 28 | document.body.appendChild(orientationBlocker) 29 | Master.mute = true 30 | } 31 | } else { 32 | if (hasBlocker){ 33 | hasBlocker = false 34 | orientationBlocker.remove() 35 | Master.mute = false 36 | } 37 | } 38 | } 39 | 40 | window.addEventListener('orientationchange', () => { 41 | if (window.orientation){ 42 | isLandscape(window.orientation) 43 | } 44 | }); 45 | 46 | if (window.screen && window.screen.orientation){ 47 | window.screen.orientation.addEventListener('change', () => { 48 | if (window.screen && window.screen.orientation){ 49 | isLandscape(window.screen.orientation.angle) 50 | } 51 | }); 52 | } 53 | 54 | //test initially 55 | function testScreenOrientation(){ 56 | if (window.screen && window.screen.orientation){ 57 | isLandscape(window.screen.orientation.angle) 58 | } else if (typeof window.orientation === 'number'){ 59 | isLandscape(window.orientation) 60 | } 61 | } 62 | 63 | //test periodically to make sure we didn't miss it 64 | setInterval(function(){ 65 | testScreenOrientation() 66 | }, 100) -------------------------------------------------------------------------------- /static/src/voice/Lines.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import SpeakBuffer from 'voice/SpeakBuffer' 18 | import AudioBuffer from 'Tone/core/Buffer' 19 | 20 | const introRate = 1.2 21 | const introPitch = -1 22 | 23 | const fillerRate = 1.3 24 | const fillerPitch = 2 25 | 26 | const lines = { 27 | 28 | } 29 | 30 | const lyrics = { 31 | /** 32 | * Spoken in error 33 | */ 34 | end : [ 35 | { 36 | text : 'I\'m too popular right now. I\'ve got to shut down.', 37 | // text : 'Too many people taking pictures right now. Please try later. Now I\'m shutting down.', 38 | rate : 0.9, 39 | pitch : fillerPitch 40 | }, 41 | { 42 | text : 'Okay, I am shutting down', 43 | rate : 0.9, 44 | pitch : fillerPitch 45 | }, 46 | { 47 | text : 'That\'s an error! Take another.', 48 | rate : 1.1, 49 | pitch : fillerPitch 50 | }, 51 | ] 52 | } 53 | 54 | // load everything 55 | for (let section in lyrics){ 56 | let lines = lyrics[section] 57 | if (Array.isArray(lines)){ 58 | lines.map((line, i) => { 59 | if (line.url){ 60 | lines[i].buffer = new AudioBuffer(`audio/voice/${line.url}`) 61 | } else { 62 | SpeakBuffer.getBuffer(line.text, line.rate, line.pitch).then( (buffer) => { 63 | lines[i].buffer = buffer 64 | }) 65 | } 66 | }) 67 | } else { 68 | SpeakBuffer.getBuffer(lines.text, lines.rate, lines.pitch).then( (buffer) => { 69 | lines.buffer = buffer 70 | }) 71 | } 72 | } 73 | 74 | export default lyrics 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /static/src/interface/Close.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import events from 'events' 18 | import 'style/close.css' 19 | const EventEmitter = events.EventEmitter 20 | 21 | export default class Close extends EventEmitter{ 22 | 23 | constructor(container){ 24 | super() 25 | 26 | this._closeButton = document.createElement('div') 27 | this._closeButton.id = 'closeButton' 28 | container.appendChild(this._closeButton) 29 | 30 | this._closeButton.addEventListener('click', (e) => { 31 | e.preventDefault() 32 | // this.end() 33 | this._closeButton.remove() 34 | this.emit('click') 35 | }) 36 | 37 | this._container = container 38 | } 39 | 40 | hide(){ 41 | this._closeButton.classList.remove('visible') 42 | } 43 | 44 | show(){ 45 | this._closeButton.classList.add('visible') 46 | } 47 | 48 | end(){ 49 | //put up a big blocking screen with the option to restart 50 | const end = document.createElement('div') 51 | end.id = 'end' 52 | this._container.appendChild(end) 53 | 54 | const textContainer = document.createElement('div') 55 | textContainer.id = 'text' 56 | end.appendChild(textContainer) 57 | 58 | 59 | const restText = document.createElement('div') 60 | restText.id = 'rest' 61 | restText.innerHTML = 'restart or
learn more
about how i work' 62 | textContainer.appendChild(restText) 63 | 64 | restText.querySelector('#clickHere').addEventListener('click', () => { 65 | this.emit('about') 66 | }) 67 | } 68 | } -------------------------------------------------------------------------------- /static/src/music/Music.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Transport from 'Tone/core/Transport' 18 | import Loop from 'Tone/event/Loop' 19 | import Background from 'music/Background' 20 | import Time from 'Tone/type/Time' 21 | import Fill from 'music/Fill' 22 | import Config from 'Config' 23 | import Master from 'Tone/core/Master' 24 | import MusicPosition from 'music/Position' 25 | 26 | Transport.bpm.value = 128 27 | 28 | export default class Music{ 29 | constructor(){ 30 | 31 | this._bgMusic = new Background() 32 | 33 | this._fill = new Fill() 34 | 35 | } 36 | 37 | start(){ 38 | let startTime = Transport.now() + 0.7 39 | Transport.start(startTime) 40 | this._bgMusic.start(startTime) 41 | Master.mute = false 42 | return startTime 43 | } 44 | 45 | stop(){ 46 | this._bgMusic.stop() 47 | this._fill.stop() 48 | // Master.volume.rampTo(-Infinity, 2) 49 | Transport.stop() 50 | Transport.clear(0) 51 | } 52 | 53 | fill(){ 54 | if (Transport.state === 'started'){ 55 | this._bgMusic.fill() 56 | this._fill.fill() 57 | } 58 | } 59 | 60 | endFill(){ 61 | if (Transport.state === 'started'){ 62 | this._bgMusic.fillEnd() 63 | this._fill.fillEnd() 64 | } 65 | return new Promise((done) => { 66 | Transport.scheduleOnce( (time) => { 67 | done(time) 68 | }, `@${Config.quantizeLevel}`) 69 | }) 70 | } 71 | 72 | /** 73 | * load the text line from the server 74 | */ 75 | loadLine(labels){ 76 | return this._speaker.loadLine(labels) 77 | } 78 | 79 | end(){ 80 | return this._bgMusic.end() 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /static/src/interface/Shutter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Player from 'Tone/source/Player' 18 | import Text from 'voice/Text' 19 | import events from 'events' 20 | import 'style/shutter.css' 21 | const EventEmitter = events.EventEmitter 22 | 23 | export default class Shutter extends EventEmitter{ 24 | 25 | constructor(container){ 26 | super() 27 | 28 | this._shutterButton = document.createElement('div') 29 | this._shutterButton.id = 'shutterButton' 30 | container.appendChild(this._shutterButton) 31 | 32 | const innerFill = document.createElement('div') 33 | innerFill.id = 'innerShutter' 34 | this._shutterButton.appendChild(innerFill) 35 | 36 | this._flashEl = document.createElement('div') 37 | this._flashEl.id = 'flash' 38 | container.appendChild(this._flashEl) 39 | 40 | this._shutterSound = new Player('./audio/shutter.mp3').toMaster() 41 | 42 | this._shutterButton.addEventListener('click', (e) => { 43 | e.preventDefault() 44 | this._flash() 45 | this.hide() 46 | this.emit('click') 47 | }) 48 | } 49 | 50 | /** 51 | * flash the screen 52 | * on the screen like you're taking a pic 53 | */ 54 | _flash(){ 55 | this._shutterSound.start() 56 | this._flashEl.className = 'visible' 57 | setTimeout(() => { 58 | this._flashEl.className = '' 59 | }, 250) 60 | } 61 | 62 | hide(){ 63 | this._shutterButton.classList.remove('visible') 64 | Text.shiftDown() 65 | } 66 | 67 | remove(){ 68 | this._shutterButton.remove() 69 | } 70 | 71 | show(){ 72 | this._shutterButton.classList.add('visible') 73 | Text.shiftUp() 74 | } 75 | } -------------------------------------------------------------------------------- /static/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | var webpack = require('webpack'); 18 | 19 | var PROD = process.argv.indexOf('-p') !== -1 20 | 21 | var precss = require('precss'); 22 | var calc = require("postcss-calc") 23 | var autoprefixer = require('autoprefixer'); 24 | 25 | module.exports = { 26 | 'context': __dirname, 27 | entry: { 28 | 'Main': 'src/FeatureTest', 29 | }, 30 | output: { 31 | filename: './build/[name].js', 32 | chunkFilename: './build/[id].js', 33 | sourceMapFilename : '[file].map', 34 | }, 35 | resolve: { 36 | root: __dirname, 37 | modulesDirectories : ['../third-party/Tone.js/', 'node_modules/tone', 'node_modules', 'src', 'third_party'], 38 | }, 39 | plugins: PROD ? [ 40 | new webpack.optimize.UglifyJsPlugin({minimize: true}) 41 | ] : [], 42 | module: { 43 | loaders: [ 44 | { 45 | test: /\.css$/, 46 | loader: 'style-loader!css-loader!postcss-loader' 47 | }, 48 | { 49 | test: /\.json$/, 50 | loader: 'json-loader' 51 | }, 52 | { 53 | test: /\.js$/, 54 | exclude: /(node_modules|Tone\.js)/, 55 | loader: 'babel', // 'babel-loader' is also a legal name to reference 56 | query: { 57 | presets: ['es2015'] 58 | } 59 | }, 60 | { 61 | test: /\.(png|gif|jpg|svg)$/, 62 | loader: 'url-loader', 63 | }, 64 | { 65 | test : /\.(ttf|eot|woff(2)?)(\?[a-z0-9]+)?$/, 66 | loader : 'file-loader?name=images/font/[hash].[ext]' 67 | // loader : 'file-loader' 68 | } 69 | ] 70 | }, 71 | postcss: function () { 72 | return [precss, autoprefixer, calc]; 73 | }, 74 | devtool: PROD ? '' : '#eval-source-map' 75 | }; -------------------------------------------------------------------------------- /static/src/music/Position.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Config from 'Config' 18 | let position = Config.startingPosition 19 | 20 | const LoopPosition = [{ 21 | start : 0, 22 | loopStart : '10m', 23 | loopEnd : '18m', 24 | }, 25 | { 26 | start : '10m', 27 | loopStart : '10m', 28 | loopEnd : '18m', 29 | }, 30 | { 31 | start : '18m', 32 | loopStart : '18m', 33 | loopEnd : '26m', 34 | }, 35 | { 36 | start : '42m', 37 | loopStart : '42m', 38 | loopEnd : '50m', 39 | }, 40 | { 41 | start : '42m', 42 | loopStart : '42m', 43 | loopEnd : '50m', 44 | }, 45 | { 46 | start : '50m', 47 | loopStart : '50m', 48 | loopEnd : '58m', 49 | }] 50 | 51 | const FillIndices = [0, 0, 1, 1, 2, 2] 52 | 53 | const FillerTextIndices = [0, 1, 2, 3, 4, 5] 54 | 55 | class MusicPosition { 56 | 57 | get position(){ 58 | return position 59 | } 60 | set position(val){ 61 | position = val 62 | } 63 | 64 | get max(){ 65 | return LoopPosition.length 66 | } 67 | 68 | get progress(){ 69 | return position / (this.max - 1) 70 | } 71 | 72 | get backgroundLoop(){ 73 | return LoopPosition[position] 74 | } 75 | 76 | get fillPosition(){ 77 | return FillIndices[position] 78 | } 79 | 80 | get fillerTextPosition(){ 81 | return FillerTextIndices[position] 82 | } 83 | 84 | get giorgioBreak(){ 85 | return false 86 | // return position === 5 87 | } 88 | 89 | get end(){ 90 | return position === (this.max - 1) 91 | } 92 | 93 | get quantizationTime(){ 94 | if (position >= 3){ 95 | return '@1m' 96 | } else { 97 | return '@4n' 98 | } 99 | } 100 | } 101 | 102 | export default new MusicPosition -------------------------------------------------------------------------------- /static/src/voice/SpeakBuffer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Buffer from 'Tone/core/Buffer' 18 | import BufferSource from 'Tone/source/BufferSource' 19 | import Text from 'voice/Text' 20 | import VoiceEffects from 'voice/VoiceEffects' 21 | import Time from 'Tone/type/Time' 22 | import Master from 'Tone/core/Master' 23 | import Tone from 'Tone/core/Tone' 24 | 25 | const currentPlaying = [] 26 | 27 | function play(text, buffer, time, effect='dry', label){ 28 | let source; 29 | return new Promise( (done) => { 30 | if (time instanceof Time){ 31 | time = time.toSeconds() 32 | } 33 | 34 | source = new BufferSource(buffer) 35 | currentPlaying.push(source) 36 | source.fadeIn = 0.01 37 | source.fadeOut = 0.01 38 | let duration = buffer.duration 39 | if (effect === 'line' || effect === 'end'){ 40 | let landTime = time + Time('4n').mult(4.67).toSeconds() 41 | let startTime = landTime - buffer.duration 42 | VoiceEffects[effect](source, time + Time('1m').toSeconds()) 43 | time = startTime 44 | if (time < Tone.now()){ 45 | time = Tone.now() 46 | } 47 | if (effect === 'end'){ 48 | duration *= 1.3 49 | } 50 | } else if (VoiceEffects.hasOwnProperty(effect)){ 51 | VoiceEffects[effect](source) 52 | } 53 | source.start(time) 54 | source.onended = done 55 | Text.show(text, time, duration, label) 56 | }).then(() => { 57 | currentPlaying.splice(currentPlaying.indexOf(source), 1) 58 | }) 59 | } 60 | 61 | function stop(time){ 62 | currentPlaying.forEach((src) => { 63 | src.stop(time) 64 | }) 65 | Text.clear() 66 | } 67 | 68 | function getBuffer(line, rate=1, pitch=0){ 69 | return new Promise((done, err) => { 70 | const buff = new Buffer(`speak/?text=${encodeURIComponent(line)}&rate=${rate}&pitch=${pitch}`, () => { 71 | done(buff) 72 | }, err) 73 | }) 74 | } 75 | 76 | export default { 77 | play, getBuffer, stop 78 | } -------------------------------------------------------------------------------- /static/style/shutter.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | #flash{ 18 | width: 100%; 19 | height: 100%; 20 | position: absolute; 21 | left: 0px; 22 | top: 0px; 23 | color: white; 24 | background-color: white; 25 | opacity: 0; 26 | transition: opacity 0.6s; 27 | pointer-events: none; 28 | 29 | &.visible { 30 | opacity: 1; 31 | transition: opacity 0.1s; 32 | } 33 | } 34 | 35 | $shutterSize : 80px; 36 | $slowTransition: 0.4s; 37 | $fastTransition: 0.25s; 38 | 39 | #shutterButton { 40 | position: absolute; 41 | bottom: 20px; 42 | left: 50%; 43 | width: $shutterSize; 44 | height: $shutterSize; 45 | margin-left: calc(-$shutterSize / 2); 46 | // border-radius: 50%; 47 | // border: 2px solid white; 48 | background-color: transparent; 49 | transition: transform $slowTransition; 50 | transform: scale(0); 51 | pointer-events: none; 52 | user-select: none; 53 | background-image: url(../images/shutter.png); 54 | background-size: 100% 100%; 55 | z-index: 100001; 56 | -webkit-tap-highlight-color: rgba(0,0,0,0); 57 | 58 | $innerSize : calc($shutterSize * 0.8); 59 | 60 | /* #innerShutter { 61 | position: absolute; 62 | width: $innerSize; 63 | height: $innerSize; 64 | left: 50%; 65 | top: 50%; 66 | margin-left: calc(-$innerSize / 2); 67 | margin-top: calc(-$innerSize / 2); 68 | background-color: white; 69 | opacity: 0.7; 70 | border-radius: 50%; 71 | transform: scale(0); 72 | transition: transform $fastTransition; 73 | } */ 74 | 75 | 76 | &.visible { 77 | pointer-events: initial; 78 | transform: scale(1); 79 | cursor: pointer; 80 | transition-duration: $fastTransition; 81 | 82 | &:hover { 83 | transform: scale(1.1); 84 | transition-duration: $fastTransition; 85 | 86 | &:active { 87 | filter: brightness(40%); 88 | } 89 | } 90 | /* #innerShutter { 91 | transition-duration: $slowTransition; 92 | transform: scale(1); 93 | } */ 94 | } 95 | } -------------------------------------------------------------------------------- /static/style/camera.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | canvas#three { 18 | opacity : 0; 19 | 20 | &.visible { 21 | opacity: 1; 22 | } 23 | } 24 | 25 | #videoContainer { 26 | position: absolute; 27 | top: 0px; 28 | left: 0px; 29 | height: 100%; 30 | width: 100%; 31 | overflow: hidden; 32 | opacity: 0; 33 | transition: opacity 0.3s; 34 | 35 | &.visible { 36 | opacity: 1; 37 | } 38 | 39 | video { 40 | position: absolute; 41 | top: 50%; 42 | left: 50%; 43 | z-index: 0; 44 | transform: translate(-50%, -50%) scale(0.01); 45 | 46 | &.tall { 47 | height: auto; 48 | width: 100%; 49 | } 50 | 51 | &.wide { 52 | height: 100%; 53 | width: auto; 54 | } 55 | 56 | &.active { 57 | transform: translate(-50%, -50%) scale(1); 58 | } 59 | } 60 | 61 | } 62 | 63 | $maxBounce : 0.3; 64 | 65 | @keyframes bounceAnimation { 66 | 0% { 67 | transform: scale(calc($maxBounce * 0 + 1)); 68 | } 69 | 10% { 70 | transform: scale(calc($maxBounce * 0.3 + 1)); 71 | } 72 | 20% { 73 | transform: scale(calc($maxBounce * 0.58 + 1)); 74 | } 75 | 30% { 76 | transform: scale(calc($maxBounce * 0.8 + 1)); 77 | } 78 | 40% { 79 | transform: scale(calc($maxBounce * 0.95 + 1)); 80 | } 81 | 50% { 82 | transform: scale(calc($maxBounce * 1 + 1)); 83 | } 84 | 60% { 85 | transform: scale(calc($maxBounce * 0.95 + 1)); 86 | } 87 | 70% { 88 | transform: scale(calc($maxBounce * 0.8 + 1)); 89 | } 90 | 80% { 91 | transform: scale(calc($maxBounce * 0.58 + 1)); 92 | } 93 | 90% { 94 | transform: scale(calc($maxBounce * 0.3 + 1)); 95 | } 96 | 100% { 97 | transform: scale(calc($maxBounce * 0 + 1)); 98 | 99 | } 100 | } 101 | 102 | #image { 103 | position: absolute; 104 | top: 0px; 105 | left: 0px; 106 | width: 100%; 107 | height: 100%; 108 | background-color: blue; 109 | pointer-events: none; 110 | opacity: 0; 111 | 112 | &.bounce { 113 | animation: bounceAnimation 0.46875s infinite; 114 | } 115 | 116 | &.visible { 117 | opacity: 1; 118 | } 119 | } -------------------------------------------------------------------------------- /static/src/music/Background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Player from 'Tone/source/Player' 18 | import Config from 'Config' 19 | import Time from 'Tone/type/Time' 20 | import MusicPosition from 'music/Position' 21 | import Transport from 'Tone/core/Transport' 22 | 23 | const backgroundVolume = -8 24 | 25 | export default class BackgroundMusic { 26 | constructor(){ 27 | this._player = new Player('../audio/racer.mp3').toMaster() 28 | this._player.volume.value = backgroundVolume 29 | 30 | const startLoop = MusicPosition.backgroundLoop 31 | // set it to loop 32 | this._player.loopStart = startLoop.loopStart 33 | this._player.loopEnd = startLoop.loopEnd 34 | this._player.loop = true 35 | 36 | //the outro music 37 | this._outro = new Player('../audio/end.mp3').toMaster() 38 | this._outro.volume.value = -2 39 | 40 | this._applause = new Player('../audio/applause.mp3').toMaster() 41 | this._applause.volume.value = -4 42 | 43 | this._ended = false 44 | 45 | } 46 | 47 | start(time){ 48 | this._player.start(time, MusicPosition.backgroundLoop.start) 49 | } 50 | 51 | fill(){ 52 | // duck the volume out immediately 53 | let stopTime = Time(`@${Config.quantizeLevel}`).toSeconds() 54 | this._player.stop(`${stopTime} + 4n`) 55 | this._player.volume.rampTo(-Infinity, '4n', stopTime) 56 | } 57 | 58 | fillEnd(){ 59 | if (!this._ended){ 60 | let startTime = Time(`@${Config.quantizeLevel}`).toSeconds() 61 | const loopPosition = MusicPosition.backgroundLoop 62 | // set it to loop 63 | this._player.loopStart = loopPosition.loopStart 64 | this._player.loopEnd = loopPosition.loopEnd 65 | this._player.start(startTime, loopPosition.start) 66 | this._player.volume.rampTo(backgroundVolume, 0.01, startTime) 67 | } 68 | } 69 | 70 | stop(){ 71 | this._ended = true 72 | const outTime = Time('@4n') 73 | this._outro.start(outTime, 0) 74 | this._outro.volume.rampTo(-Infinity, '4m', outTime) 75 | this._player.volume.rampTo(-Infinity, '2n') 76 | } 77 | 78 | end(){ 79 | this._ended = true 80 | this._player.volume.rampTo(-Infinity, Config.fadeOutTime) 81 | this._applause.start() 82 | } 83 | } -------------------------------------------------------------------------------- /server/mary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2016 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import webapp2 19 | import urllib2 20 | 21 | from google.appengine.api import memcache 22 | 23 | MARY_TTS_URL = 'http://127.0.0.1' 24 | MARY_TTS_PORT = '59125' 25 | 26 | 27 | class SpeakHandler(webapp2.RequestHandler): 28 | def maryRequestUrl(self, text, rate, pitch): 29 | f0_scale = '0.8' if float(pitch) == 0 else '1.5' 30 | tract_scaler = '0.9' 31 | f0_add = str(float(pitch) * 10) 32 | text = urllib2.quote(text.encode("utf-8")) 33 | urlString = '{0}:{1}/process?INPUT_TYPE=TEXT&OUTPUT_TYPE=AUDIO&INPUT_TEXT={2}'.format(MARY_TTS_URL, MARY_TTS_PORT, text) 34 | urlString += '&VOICE_SELECTIONS=cmu-bdl-hsmm%20en_US%20male%20hmm&AUDIO_OUT=WAVE_FILE&LOCALE=en_US&VOICE=cmu-bdl-hsmm&AUDIO=WAVE_FILE' 35 | # some voice parameters 36 | urlString += '&effect_F0Scale_selected=on&effect_F0Scale_parameters=f0Scale%3A{0}%3B'.format(f0_scale) 37 | urlString += '&effect_TractScaler_selected=on&effect_TractScaler_parameters=amount%3A{0}%3B'.format(tract_scaler) 38 | urlString += '&effect_F0Add_selected=on&effect_F0Add_parameters=f0Add%3A{0}%3B'.format(f0_add) 39 | urlString += '&effect_Rate_selected=on&effect_Rate_parameters=durScale%3A{0}%3B'.format(str(1 / float(rate))) 40 | return urlString 41 | def get(self): 42 | text = self.request.get('text', default_value='this is a test') 43 | rate = self.request.get('rate', default_value='1') 44 | pitch = self.request.get('pitch', default_value='0') 45 | 46 | key = 'text={0} rate={1} pitch={2}'.format(text, rate, pitch) 47 | print self.maryRequestUrl(text, rate, pitch) 48 | audio = memcache.get(key) 49 | if audio is None: 50 | url = self.maryRequestUrl(text, rate, pitch) 51 | audio = urllib2.urlopen(url).read() 52 | four_hours = 60 * 60 * 4 53 | memcache.add(key, audio, four_hours) 54 | self.response.headers['Content-Type'] = 'audio/wav' 55 | self.response.out.write(audio) 56 | 57 | 58 | app = webapp2.WSGIApplication([ 59 | ('/.*', SpeakHandler) 60 | ], debug=True) 61 | -------------------------------------------------------------------------------- /static/style/about.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | @import 'common.css'; 18 | 19 | $pink : #ff52b8; 20 | 21 | #about { 22 | background-color: black; 23 | width: 100%; 24 | height: 100%; 25 | z-index: 1000000; 26 | top: 0px; 27 | left: 0px; 28 | position: absolute; 29 | overflow-y: auto; 30 | display: none; 31 | 32 | &.visible { 33 | display: initial; 34 | } 35 | 36 | #close { 37 | @mixin closeButton; 38 | } 39 | 40 | #content { 41 | width: 90%; 42 | margin: 30px auto 30px auto; 43 | min-width: 300px; 44 | max-width: 800px; 45 | height: auto; 46 | position: relative; 47 | color: white; 48 | font-family: 'Pixel Operator', monospace; 49 | position: relative; 50 | 51 | #title { 52 | font-family: 'Pixel Operator Bold', monospace; 53 | font-size: 40px; 54 | line-height: 30px; 55 | margin: 20px 0px 30px 0px; 56 | position: relative; 57 | } 58 | 59 | $smallScreen : 400px; 60 | $mediumScreen : 800px; 61 | 62 | #video { 63 | width: 100%; 64 | cursor: pointer; 65 | position: relative; 66 | 67 | @media (max-width: $smallScreen) { 68 | height: 200px; 69 | } 70 | 71 | @media (min-width: $smallScreen) and (max-width: $mediumScreen) { 72 | height: 300px; 73 | } 74 | 75 | @media (min-width: $mediumScreen){ 76 | height: 400px; 77 | } 78 | 79 | iframe { 80 | width: 100%; 81 | height: 100%; 82 | } 83 | 84 | #playButton { 85 | pointer-events: none; 86 | position: absolute; 87 | top: 50%; 88 | left: 50%; 89 | transform: translate(-50%, -50%); 90 | width: 100px; 91 | height: 100px; 92 | background-image: url(../images/play_button.svg); 93 | background-size: 100% 100%; 94 | display: none; 95 | 96 | &.visible { 97 | display: initial; 98 | } 99 | } 100 | 101 | &:hover #playButton svg{ 102 | fill: blue; 103 | } 104 | } 105 | 106 | #blurb { 107 | 108 | position: relative; 109 | text-transform: none; 110 | margin-top: 30px; 111 | font-size: 20px; 112 | line-height: 25px; 113 | 114 | a { 115 | @mixin yellowLink; 116 | color: $pink; 117 | } 118 | } 119 | 120 | } 121 | } -------------------------------------------------------------------------------- /static/src/voice/Filler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import SpeakBuffer from 'voice/SpeakBuffer' 18 | import Loop from 'Tone/event/Loop' 19 | import Time from 'Tone/type/Time' 20 | import Lines from 'voice/Lines' 21 | import MusicPosition from 'music/Position' 22 | 23 | //special thanks to giorgio moroder, here he is on the vocoder 24 | 25 | const fillerPhrases = [ 26 | ['Snap a photo', 'I am ready', 'Waiting for you'], 27 | ['Okay what else?', 'Turn it up', 'Yeah', 'Snap another', 'Image Recognition, yeah'], 28 | ['Turn it up', 'Speech Synthesis, yeah', 'Don\'t stop', 'This is Awesome'], 29 | ['Woah', 'Keep it going', 'Take another', 'This is Crazy'], 30 | ['One more time', 'Last one', 'Alright', 'Yeah'], 31 | ['That was awesome', 'I am shutting down'], 32 | ] 33 | 34 | export default class Filler { 35 | constructor(){ 36 | 37 | this._loop = new Loop(this._speak.bind(this), '2m') 38 | 39 | this._phraseIndex = -1 40 | 41 | this._phrases = [] 42 | 43 | fillerPhrases.forEach((phrases, i) => { 44 | this._phrases[i] = [] 45 | phrases.forEach((text, j) => { 46 | SpeakBuffer.getBuffer(text, 1.3, 2).then( (buffer) => { 47 | this._phrases[i][j] = {text, buffer} 48 | }) 49 | }) 50 | }) 51 | } 52 | 53 | start(){ 54 | if (this._loop.state === 'stopped'){ 55 | this._phraseIndex = 0 56 | if (MusicPosition.end){ 57 | this._loop.interval = '1m' 58 | } 59 | if (MusicPosition.position === 0){ 60 | this._loop.start("@1m + 1m") 61 | } else { 62 | this._loop.start("@1m") 63 | } 64 | } 65 | } 66 | 67 | stop(){ 68 | this._loop.stop() 69 | } 70 | 71 | _chooseRandom(){ 72 | //randomness excludes the first result 73 | const random = Math.floor(Math.random() * (this._phrases[MusicPosition.fillerTextPosition].length - 1)) + 1 74 | if (random === this._phraseIndex){ 75 | return this._chooseRandom() 76 | } else { 77 | return random 78 | } 79 | 80 | } 81 | 82 | _speak(time){ 83 | const phrase = this._phrases[MusicPosition.fillerTextPosition][this._phraseIndex] 84 | if (phrase){ 85 | SpeakBuffer.play(phrase.text, phrase.buffer, Time(time), 'fill') 86 | if (MusicPosition.end){ 87 | this._phraseIndex++ 88 | this._loop.stop('+2m + 4n') 89 | } else { 90 | this._phraseIndex = this._chooseRandom() 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /server/vision.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2016 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | import webapp2 19 | import urllib2 20 | import json 21 | 22 | import base64 23 | 24 | from google.appengine.ext import vendor 25 | vendor.add('server/lib') 26 | 27 | import re 28 | from googleapiclient import discovery 29 | from oauth2client.client import GoogleCredentials 30 | 31 | # remove these things cause it could be taken the wrong way 32 | DISCOVERY_URL='https://{api}.googleapis.com/$discovery/rest?version={apiVersion}' 33 | 34 | class VisionApi(webapp2.RequestHandler): 35 | def __init__(self, request, response): 36 | # Set self.request, self.response and self.app. 37 | self.initialize(request, response) 38 | self.vision = self._create_client() 39 | 40 | def _create_client(self): 41 | credentials = GoogleCredentials.get_application_default() 42 | return discovery.build( 43 | 'vision', 'v1', credentials=credentials, 44 | discoveryServiceUrl=DISCOVERY_URL) 45 | 46 | def _label(self, image, max_results=10, num_retries=2): 47 | """ 48 | Uses the Vision API to detect text in the given file. 49 | """ 50 | 51 | label_request = { 52 | 'image': { 53 | 'content': image 54 | }, 55 | 'features': [{ 56 | 'type': 'LABEL_DETECTION', 57 | 'maxResults': max_results, 58 | }] 59 | } 60 | 61 | request = self.vision.images().annotate(body={'requests': [label_request]}) 62 | response = request.execute(num_retries=num_retries)['responses'][0] 63 | labels_raw = response.get('labelAnnotations', []) 64 | print labels_raw 65 | error = response.get('error', []) 66 | if len(labels_raw): 67 | filtered = [r for r in labels_raw if float(r['score']) > 0.1] 68 | labels = [{'label' : str(x['description']), 'score' : float(x['score'])} for x in filtered] 69 | return labels 70 | elif len(error): 71 | return {'error' : error} 72 | else: 73 | return [] 74 | 75 | 76 | def post(self): 77 | image = self.request.POST.multi['image'] 78 | labels = self._label(image) 79 | self.response.out.write(json.dumps(labels)) 80 | 81 | 82 | app = webapp2.WSGIApplication([ 83 | ('.*', VisionApi) 84 | ], debug=True) -------------------------------------------------------------------------------- /static/src/voice/Lyrics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import MusicPosition from 'music/Position' 18 | 19 | const couplets = [ 20 | ['Is that % I see?', 'Looks like % to me'], 21 | ['Is that % or not?', 'Got % in this spot'], 22 | // ['%, that\'s what I say', 'Looks like % to me'], 23 | // ['I see %, right?', 'Got % in my sight'], 24 | ['That\'s % on my screen', 'I think % is what I\'ve seen'], 25 | ['Looks like % to me', 'I\'m seeing %, probably'], 26 | ['I think % is what we\'ve got', 'Could be %, but maybe not'], 27 | ] 28 | 29 | const confused = [ 30 | ['What is that i see?', 'This image is confusing me'], 31 | // ['Take another pic, okay?', 'Rocking Google Cloud Vision all day'], 32 | ['I D K', 'Take another pic, okay?'], 33 | ['I\'m not quite sure what we\'ve got', 'Please take another shot'] 34 | ] 35 | 36 | let history = [] 37 | let lastLineIndex = -1 38 | 39 | /** 40 | * Get a random line which includes the labels. 41 | * Make sure it has a different index than the last one. 42 | */ 43 | function randomLine(labels){ 44 | let randIndex = Math.floor(Math.random() * couplets.length) 45 | //make sure you don't repeat the line 46 | if (randIndex === lastLineIndex){ 47 | return randomLine(labels) 48 | } 49 | lastLineIndex = randIndex 50 | let randLine = couplets[randIndex] 51 | randLine = randLine.slice() 52 | randLine[0] = randLine[0].replace(/%/g, labels[0].label) 53 | randLine[1] = randLine[1].replace(/%/g, labels[1].label) 54 | return randLine 55 | } 56 | 57 | /** 58 | * Given the array of labels in this form: 59 | * [ { 60 | * label : 'label text', 61 | * score : Number 62 | * }, ...] 63 | * Returns two lines which includes one of those labels and the labels 64 | * that it used 65 | * @returns {Object} {lines : [], labels : []} 66 | */ 67 | function getLine(labels){ 68 | if (labels.length > 1){ 69 | //choose a label that haven't been used in a while 70 | //reverse sort the labels by their position in the history 71 | labels = labels.sort((a, b) => { 72 | let aIndex = history.findIndex((l) => l.label === a.label) + 1 73 | let bIndex = history.findIndex((l) => l.label === b.label) + 1 74 | return aIndex - bIndex 75 | }) 76 | let retLabels = labels.slice(0, 2) 77 | history = history.concat(retLabels) 78 | // put it in the line 79 | return { lines : randomLine(retLabels), labels : retLabels} 80 | } else { 81 | let randLine = confused[Math.floor(Math.random() * confused.length)] 82 | if (MusicPosition.end){ 83 | //doesn't make sense to say take another shot at the very end 84 | randLine = confused[0] 85 | } 86 | return {lines: randLine.slice(), labels : []} 87 | } 88 | } 89 | 90 | 91 | export default {getLine} -------------------------------------------------------------------------------- /static/src/voice/VoiceEffects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Convolver from 'Tone/effect/Convolver' 18 | import Compressor from 'Tone/component/Compressor' 19 | import PingPongDelay from 'Tone/effect/PingPongDelay' 20 | import FeedbackDelay from 'Tone/effect/FeedbackDelay' 21 | import PitchShift from 'Tone/effect/PitchShift' 22 | import Gain from 'Tone/core/Gain' 23 | import Tone from 'Tone/core/Tone' 24 | 25 | //a master out object so that they can all be stopped at once 26 | const VoiceMasterOut = new Gain().toMaster() 27 | 28 | const delay = new PingPongDelay('4n', 0.4).connect(VoiceMasterOut) 29 | delay.wet.value = 0.8 30 | 31 | const delayInput = new Gain().connect(delay) 32 | delayInput.gain.value = 0 33 | 34 | const comp = new Compressor().connect(VoiceMasterOut) 35 | comp.connect(delayInput) 36 | 37 | const fillDelay = new FeedbackDelay({ 38 | delayTime : '4n', 39 | feedback : 0.25, 40 | wet : 0.3, 41 | }).connect(VoiceMasterOut) 42 | 43 | const reverseVerb = new Convolver({ 44 | url : 'audio/reverseCrash.mp3', 45 | wet : 1 46 | }).connect(fillDelay) 47 | 48 | 49 | export default { 50 | dry : function(source){ 51 | source.connect(VoiceMasterOut) 52 | }, 53 | 54 | fill : function(source){ 55 | source.connect(VoiceMasterOut) 56 | // source.connect(fillDelay) 57 | source.connect(reverseVerb) 58 | }, 59 | 60 | wait : function(source){ 61 | source.connect(VoiceMasterOut) 62 | // source.connect(fillDelay) 63 | source.connect(reverseVerb) 64 | }, 65 | 66 | line : function(source, effectTime){ 67 | source.connect(comp) 68 | this._delay(effectTime) 69 | }, 70 | 71 | down : function(source, effectTime){ 72 | let rampTime = source.buffer.duration * 0.25 73 | source.playbackRate.exponentialRampToValue(0.7, rampTime, Tone.now() + source.buffer.duration * 0.75) 74 | source.toMaster() 75 | source.connect(reverseVerb) 76 | }, 77 | 78 | end : function(source, effectTime){ 79 | const pitchDelay = new PitchShift({ 80 | delayTime : '4n', 81 | feedback : 0.4, 82 | wet : 0, 83 | pitch : -2, 84 | windowSize : 0.05 85 | }).toMaster() 86 | let rampTime = source.buffer.duration * 0.25 87 | pitchDelay.wet.setValueAtTime(0.5, effectTime - rampTime / 2) 88 | source.connect(pitchDelay) 89 | source.playbackRate.exponentialRampToValue(0.7, rampTime, effectTime - rampTime) 90 | source.toMaster() 91 | }, 92 | 93 | _delay : function(time){ 94 | delayInput.gain.setValueAtTime(1, time) 95 | delayInput.gain.linearRampToValueAtTime(0, time + 2) 96 | }, 97 | 98 | stop : function(){ 99 | VoiceMasterOut.gain.rampTo(0, 0.8) 100 | } 101 | } -------------------------------------------------------------------------------- /static/src/interface/Splash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import 'style/splash.css' 18 | import events from 'events' 19 | const EventEmitter = events.EventEmitter 20 | import Loader from 'interface/Loader' 21 | import Config from 'Config' 22 | import screenfull from 'screenfull' 23 | import Title from 'interface/Title' 24 | 25 | 26 | const TitleColors = ['pink', 'purple', 'yellow', 'teal', 'purple', 'yellow', 'teal', 'space', 'pink', 'yellow', 'teal'] 27 | 28 | export default class Splash extends EventEmitter{ 29 | constructor(container){ 30 | super() 31 | 32 | this._splashElement = document.createElement('div') 33 | this._splashElement.id = 'splash' 34 | container.appendChild(this._splashElement) 35 | 36 | // the title 37 | const titleContainer = document.createElement('div') 38 | titleContainer.id = 'titleContainer' 39 | this._splashElement.appendChild(titleContainer) 40 | 41 | const title = new Title(titleContainer) 42 | 43 | const subTitle = document.createElement('div') 44 | subTitle.id = 'subTitle' 45 | titleContainer.appendChild(subTitle) 46 | subTitle.textContent = 'Use your camera to make music with me.' 47 | 48 | // the loader / ready button 49 | this._loader = new Loader(titleContainer) 50 | 51 | this._loader.on('start', () => { 52 | 53 | this.emit('start') 54 | this._splashElement.classList.add('disappear') 55 | 56 | if (screenfull.enabled && Config.fullscreen) { 57 | screenfull.request() 58 | } 59 | }) 60 | 61 | const learnMore = document.createElement('a') 62 | learnMore.id = 'learnMore' 63 | learnMore.textContent = 'About' 64 | titleContainer.appendChild(learnMore) 65 | learnMore.addEventListener('click', (e) => { 66 | e.preventDefault() 67 | this.emit('about') 68 | }) 69 | 70 | // logos at the bottom 71 | const aiExperiments = document.createElement('a') 72 | aiExperiments.id = 'aiExperiments' 73 | aiExperiments.href = Config.aiExperimentsLink 74 | aiExperiments.target = '_blank' 75 | this._splashElement.appendChild(aiExperiments) 76 | 77 | // break 78 | const badgeBreak = document.createElement('div') 79 | badgeBreak.id = 'badgeBreak' 80 | this._splashElement.appendChild(badgeBreak) 81 | 82 | const googleFriends = document.createElement('a') 83 | googleFriends.id = 'googleFriends' 84 | this._splashElement.appendChild(googleFriends) 85 | 86 | const privacyAndTerms = document.createElement('div') 87 | privacyAndTerms.id = 'privacyAndTerms' 88 | privacyAndTerms.innerHTML = 'privacy&terms' 89 | this._splashElement.appendChild(privacyAndTerms) 90 | 91 | } 92 | 93 | show(){ 94 | this._splashElement.classList.remove('disappear') 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /static/src/interface/About.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import 'style/about.css' 18 | import Config from 'Config' 19 | import YouTubeIframeLoader from 'youtube-iframe' 20 | 21 | const maryLink = 'https://github.com/marytts/marytts' 22 | const cloudVisionLink = 'https://cloud.google.com/vision/' 23 | const sourceCode = 'https://github.com/googlecreativelab/aiexperiments-giorgio-cam' 24 | 25 | const blurbCopy = `Built by Eric Rosenbaum, Yotam Mann, and friends at Google Creative Lab 26 | using Google Cloud Vision API and MaryTTS. 27 | The open-source code is available here. Check out more 28 | A.I. Experiments.` 29 | 30 | export default class About { 31 | constructor(container){ 32 | 33 | this._container = document.createElement('div') 34 | this._container.id = 'about' 35 | container.appendChild(this._container) 36 | 37 | const closeButton = document.createElement('div') 38 | closeButton.id = 'close' 39 | closeButton.classList.add('visible') 40 | this._container.appendChild(closeButton) 41 | closeButton.addEventListener('click', (e) => { 42 | e.preventDefault() 43 | this.close() 44 | }) 45 | 46 | const content = document.createElement('div') 47 | content.id = 'content' 48 | this._container.appendChild(content) 49 | 50 | const title = document.createElement('div') 51 | title.id = 'title' 52 | title.textContent = Config.name 53 | content.appendChild(title) 54 | 55 | 56 | const video = document.createElement('div') 57 | video.id = 'video' 58 | //vid YT0k99hCY5I 59 | video.innerHTML = `` 60 | content.appendChild(video) 61 | 62 | this._ytplayer = null 63 | 64 | this._playButton = document.createElement('div') 65 | this._playButton.id = 'playButton' 66 | this._playButton.classList.add('visible') 67 | video.appendChild(this._playButton) 68 | 69 | YouTubeIframeLoader.load((YT) => { 70 | this._ytplayer = new YT.Player('youtube-iframe', { 71 | events : { 72 | onStateChange : (state) => { 73 | this._playButton.classList.remove('visible') 74 | } 75 | } 76 | }) 77 | }) 78 | 79 | const blurb = document.createElement('div') 80 | blurb.id = 'blurb' 81 | content.appendChild(blurb) 82 | blurb.innerHTML = blurbCopy 83 | 84 | } 85 | close(){ 86 | this._container.classList.remove('visible') 87 | if (this._ytplayer){ 88 | this._ytplayer.stopVideo() 89 | } 90 | if (ga){ 91 | ga('send', 'event', 'GiorgioCam', 'Click', 'About - Close') 92 | } 93 | } 94 | open(){ 95 | this._playButton.classList.add('visible') 96 | this._container.classList.add('visible') 97 | if (ga){ 98 | ga('send', 'event', 'GiorgioCam', 'Click', 'About - Open') 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /static/src/voice/Wait.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import SpeakBuffer from 'voice/SpeakBuffer' 18 | import Loop from 'Tone/event/Loop' 19 | import Time from 'Tone/type/Time' 20 | import AudioBuffer from 'Tone/core/Buffer' 21 | import MusicPosition from 'music/Position' 22 | import Player from 'Tone/source/Player' 23 | import Sequence from 'Tone/event/Sequence' 24 | import Config from 'Config' 25 | 26 | const openingLines = [ 27 | {text : 'H h here we go', url : 'herewego.mp3'}, 28 | {text : 'y y y-y yeah' , url : 'yeah.mp3'}, 29 | {text : 'a alright', url : 'alright.mp3'}, 30 | {text : 'yeah yeah yeah yeah yeah-yeah yeah-yeah yeah-yeah yeah-yeah', url : 'yeahup.mp3'}, 31 | {text : 'here we go go go go go-go go-go go-go go-go', url : 'herewegogo.mp3'}, 32 | ] 33 | 34 | const soundEffects = new Player({url : 'audio/siren.mp3', volume : -6}).toMaster() 35 | 36 | const waitingLines = ['I\'m on it', 'Alright', 'Working on it', 'Almost there', 'Working on it', 'Almost there'] 37 | 38 | export default class Wait { 39 | constructor(){ 40 | 41 | // this._loop = new Loop(this._speak.bind(this), '1m') 42 | // this._loop.mute = true 43 | 44 | this._phraseIndex = 0 45 | 46 | this._phrases = [] 47 | openingLines.forEach((desc, i) => { 48 | const text = desc.text 49 | if (desc.url){ 50 | const buffer = new AudioBuffer(`audio/voice/${desc.url}`) 51 | this._phrases[i] = {text, buffer} 52 | } else { 53 | SpeakBuffer.getBuffer(text, 1.2, 2.5).then((buffer) => { 54 | this._phrases[i] = {text, buffer} 55 | }) 56 | } 57 | }) 58 | 59 | waitingLines.forEach((text, i) => { 60 | SpeakBuffer.getBuffer(text, 1.2, 2).then((buffer) => { 61 | waitingLines[i] = {text, buffer} 62 | }) 63 | }) 64 | this._waitSequence = new Sequence(this._playNextWait.bind(this), [0, 1, 2, 3, 4, 5], '1m') 65 | } 66 | 67 | start(){ 68 | let phrase = this._phrases[MusicPosition.position - 1] 69 | SpeakBuffer.play(phrase.text, phrase.buffer, Time('@4n + 4n'), 'wait') 70 | if (MusicPosition.position >= 4){ 71 | soundEffects.start('@4n + 4n') 72 | } 73 | this._waitSequence.start('@1m + 1m') 74 | } 75 | 76 | stop(){ 77 | if (soundEffects.state === 'started'){ 78 | soundEffects.stop(`@${Config.quantizeLevel}`) 79 | } 80 | this._waitSequence.stop() 81 | SpeakBuffer.stop(`@${Config.quantizeLevel}`) 82 | } 83 | 84 | _chooseRandom(){ 85 | let randIndex = Math.floor(Math.random() * this._phrases.length) 86 | if (randIndex === this._phraseIndex){ 87 | return this._chooseRandom() 88 | } else { 89 | return randIndex 90 | } 91 | } 92 | 93 | _playNextWait(time, index){ 94 | const phrase = waitingLines[index] 95 | SpeakBuffer.play(phrase.text, phrase.buffer, Time(time), 'wait') 96 | } 97 | 98 | _speak(time){ 99 | //choose a phrase at random 100 | let phrase = this._phrases[this._phraseIndex] 101 | SpeakBuffer.play(phrase.text, phrase.buffer, Time(time), 'wait') 102 | this._phraseIndex = (this._phraseIndex + 1) % this._phrases.length 103 | // this._phraseIndex = this._chooseRandom() 104 | } 105 | } -------------------------------------------------------------------------------- /static/src/camera/Camera.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import 'webrtc-adapter' 18 | import mobile from 'is-mobile' 19 | import 'style/camera.css' 20 | import Label from 'camera/Label' 21 | import Config from 'Config' 22 | import Canvas from 'camera/Canvas' 23 | 24 | export default class Camera { 25 | constructor(container) { 26 | 27 | this._canvas = new Canvas(container) 28 | 29 | this._video = document.createElement('video') 30 | window.video = this._video 31 | 32 | this._constraints = { 33 | audio: false, 34 | video: { 35 | facingMode : { ideal: 'environment' } 36 | } 37 | } 38 | 39 | this._frontFacing = !mobile() 40 | } 41 | 42 | open(){ 43 | 44 | let tries = 0 45 | return navigator.mediaDevices.getUserMedia(this._constraints).then((stream) => { 46 | 47 | window.stream = stream 48 | 49 | // stream. 50 | this._video.srcObject = stream 51 | this._video.autoplay = true 52 | 53 | this._video.classList.add('visible') 54 | this._canvas.setVideo(this._video) 55 | 56 | window.addEventListener('resize', this._resize.bind(this)) 57 | //size it initially 58 | this._resize() 59 | }) 60 | } 61 | 62 | error(){ 63 | this.close() 64 | this._canvas.fail() 65 | } 66 | 67 | end(){ 68 | return this._canvas.fail(Config.fadeOutTime * 1000) 69 | } 70 | 71 | close(){ 72 | this._video.pause() 73 | return this._canvas.fail().then( () => { 74 | this._video.srcObject.getVideoTracks()[0].stop() 75 | }) 76 | } 77 | 78 | _getImage(){ 79 | const video = this._video 80 | if (video){ 81 | const w = video.videoWidth 82 | const h = video.videoHeight 83 | const canvas = document.createElement('canvas') 84 | const context = canvas.getContext('2d') 85 | // take a snapshot of the incoming image 86 | canvas.width = w 87 | canvas.height = h 88 | context.drawImage(video, 0, 0, w, h) 89 | return canvas 90 | } 91 | } 92 | 93 | label() { 94 | return Label(this._getImage()) 95 | } 96 | 97 | /** 98 | * Set the sizing of the video box 99 | */ 100 | _resize(){ 101 | let aspectRatio = this._video.videoWidth / this._video.videoHeight 102 | if (isNaN(aspectRatio)){ 103 | setTimeout(() => this._resize(), 100) 104 | } else { 105 | let windowRatio = window.innerWidth / window.innerHeight 106 | this._video.classList.add('active') 107 | if (aspectRatio > windowRatio){ 108 | this._video.classList.add('wide') 109 | this._video.classList.remove('tall') 110 | } else { 111 | this._video.classList.add('tall') 112 | this._video.classList.remove('wide') 113 | } 114 | this._canvas.resize(this._video.videoWidth, this._video.videoHeight) 115 | 116 | if (this._frontFacing){ 117 | this._canvas.mirror() 118 | } 119 | } 120 | } 121 | 122 | takePicture(){ 123 | this._video.pause() 124 | this._canvas.changeColor() 125 | } 126 | 127 | resume(){ 128 | this._canvas.endBounce() 129 | this._canvas.endChangeColor() 130 | const promise = this._video.play() 131 | if (promise){ 132 | return promise 133 | } else { 134 | return Promise.resolve() 135 | } 136 | } 137 | 138 | bounce(){ 139 | this._canvas.bounce() 140 | } 141 | 142 | changeColor(){ 143 | this._canvas.changeColor() 144 | } 145 | 146 | giorgioBreak(){ 147 | this._canvas.changeColor() 148 | } 149 | 150 | } -------------------------------------------------------------------------------- /static/src/Main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import 'style/main.css' 18 | import Splash from 'interface/Splash' 19 | import Camera from 'camera/Camera' 20 | import Shutter from 'interface/Shutter' 21 | import Music from 'music/Music' 22 | import Voice from 'voice/Voice' 23 | import Text from 'voice/Text' 24 | import Config from 'Config' 25 | import Close from 'interface/Close' 26 | import OrientationListener from 'interface/Orientation' 27 | import NoCamera from 'interface/NoCamera' 28 | import About from 'interface/About' 29 | import MusicPosition from 'music/Position' 30 | import WhenVisible from 'Visible' 31 | 32 | 33 | // set the title 34 | document.title = Config.name 35 | 36 | const camera = new Camera(document.body) 37 | const shutter = new Shutter(document.body) 38 | const splash = new Splash(document.body) 39 | const closeButton = new Close(document.body) 40 | const about = new About(document.body) 41 | 42 | const music = new Music() 43 | const voice = new Voice() 44 | 45 | 46 | splash.on('start', () => { 47 | 48 | Text.appendTo(document.body) 49 | 50 | camera.open().then( () => { 51 | closeButton.show() 52 | const time = music.start() 53 | return voice.intro(time) 54 | }).then( () => { 55 | voice.fill() 56 | shutter.show() 57 | }).catch((e) => { 58 | NoCamera(document.body) 59 | }) 60 | }) 61 | 62 | splash.on('about', () => { 63 | about.open() 64 | }) 65 | 66 | closeButton.on('about', () => { 67 | about.open() 68 | }) 69 | 70 | closeButton.on('click', () => { 71 | Text.clear() 72 | voice.stop() 73 | music.stop() 74 | camera.close().then( () => { 75 | closeButton.end() 76 | }) 77 | shutter.remove() 78 | }) 79 | 80 | let retries = 0 81 | 82 | shutter.on('click', () => { 83 | 84 | //increment the position 85 | MusicPosition.position++ 86 | 87 | music.fill() 88 | voice.endFill() 89 | 90 | camera.takePicture() 91 | 92 | camera.label().then((labels) => { 93 | 94 | return voice.load(labels) 95 | 96 | }).then((lines) => { 97 | // this is so the music doesn't drop out when the transition happens 98 | // while in a background tab 99 | return WhenVisible(lines) 100 | }).then((lines) => { 101 | 102 | voice.endWait() 103 | 104 | return music.endFill().then( (time) => { 105 | camera.bounce() 106 | return voice.speak(lines, time) 107 | }) 108 | 109 | }).then(() => { 110 | voice.fill() 111 | shutter.show() 112 | return camera.resume() 113 | }).then(() => { 114 | //ENDING 115 | if (MusicPosition.end){ 116 | Text.clear() 117 | camera.end().then(() => { 118 | camera.close() 119 | closeButton.end() 120 | }) 121 | music.end() 122 | closeButton.hide() 123 | shutter.remove() 124 | } 125 | }).catch((err) => { 126 | 127 | WhenVisible().then(() => { 128 | // errorr!!! 129 | console.log(err) 130 | 131 | MusicPosition.position-- 132 | Text.clear() 133 | voice.endWait() 134 | 135 | retries++ 136 | if (retries > 2){ 137 | camera.error() 138 | closeButton.hide() 139 | voice.error().then(() => { 140 | closeButton.end() 141 | }) 142 | music.stop() 143 | } else { 144 | 145 | music.endFill().then((time) => { 146 | return voice.tryAgain(time) 147 | }).then( () => { 148 | camera.resume() 149 | shutter.show() 150 | }) 151 | } 152 | }) 153 | }) 154 | }) -------------------------------------------------------------------------------- /static/src/voice/Voice.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Intro from 'voice/Intro' 18 | import Filler from 'voice/Filler' 19 | import Wait from 'voice/Wait' 20 | import Lines from 'voice/Lines' 21 | import SpeakBuffer from 'voice/SpeakBuffer' 22 | import VoiceEffects from 'voice/VoiceEffects' 23 | import Lyrics from 'voice/Lyrics' 24 | import Config from 'Config' 25 | import Transport from 'Tone/core/Transport' 26 | import Time from 'Tone/type/Time' 27 | 28 | export default class Voice { 29 | constructor(){ 30 | this._intro = new Intro() 31 | 32 | this._filler = new Filler() 33 | 34 | this._wait = new Wait() 35 | } 36 | 37 | intro(time){ 38 | return this._intro.start(time) 39 | } 40 | 41 | fill(){ 42 | this._filler.start() 43 | } 44 | 45 | endFill(){ 46 | this._filler.stop() 47 | this.wait() 48 | } 49 | 50 | wait(){ 51 | this._wait.start() 52 | } 53 | 54 | endWait(){ 55 | this._wait.stop() 56 | } 57 | 58 | stop(){ 59 | VoiceEffects.stop() 60 | const endLines = Lines.end 61 | return SpeakBuffer.play(endLines[1].text, endLines[1].buffer, Time(), 'end') 62 | } 63 | 64 | //speak the error lines 65 | error(){ 66 | const errorLines = Lines.end 67 | return SpeakBuffer.play(errorLines[0].text, errorLines[0].buffer, Time('+0:3'), 'end') 68 | .then(() => { 69 | // add a little time at the end 70 | return new Promise((done) => { 71 | setTimeout(done, 3000) 72 | }) 73 | }) 74 | } 75 | 76 | tryAgain(time){ 77 | const errorLines = Lines.end 78 | return SpeakBuffer.play(errorLines[2].text, errorLines[2].buffer, time, 'line') 79 | .then(() => { 80 | // add a little time at the end 81 | return new Promise((done) => { 82 | setTimeout(done, 600) 83 | }) 84 | }) 85 | } 86 | 87 | openCamera(){ 88 | const errorLines = Lines.end 89 | return SpeakBuffer.play(errorLines[3].text, errorLines[3].buffer, Time('+0.1'), 'dry') 90 | .then(() => { 91 | // add a little time at the end 92 | return new Promise((done) => { 93 | setTimeout(done, 1200) 94 | }) 95 | }) 96 | } 97 | 98 | /** 99 | * Formats the return promise 100 | * { 101 | * text : 'text here', 102 | * buffer : Tone.Buffer, 103 | * label : {} // label obj 104 | * } 105 | */ 106 | _loadLine(text, label){ 107 | return SpeakBuffer.getBuffer(text, 1.1, 1).then( (buffer) => { 108 | return { text, label, buffer } 109 | }) 110 | } 111 | 112 | /** 113 | * Loads the lines, resolves the promise at the beginning of the next phrase. 114 | * Returns both lines in this format: 115 | * [ 116 | * { 117 | * text : 'text here', 118 | * buffer : Tone.Buffer, 119 | * label : {} // label obj 120 | * } 121 | * ] 122 | */ 123 | load(labels){ 124 | let lyrics = Lyrics.getLine(labels) 125 | return Promise.all([ 126 | this._loadLine(lyrics.lines[0], lyrics.labels[0]), 127 | this._loadLine(lyrics.lines[1], lyrics.labels[1]) 128 | ]) 129 | } 130 | 131 | _speakLine(line, time){ 132 | return SpeakBuffer.play(line.text, line.buffer, time, 'line', line.label) 133 | } 134 | 135 | speak(lines, time){ 136 | this._speakLine(lines[0], Time(time)) 137 | return this._speakLine(lines[1], Time(time).add('2m')).then( () => { 138 | // add a short timeout at the end 139 | return new Promise((done) => { 140 | setTimeout(done, 1400) 141 | }) 142 | }) 143 | } 144 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Giorgio Cam 2 | Take a picture to make music with the computer. 3 | 4 | ## About 5 | 6 | This is an experiment built with machine learning that lets you make music with the computer just by taking a picture. It uses image recognition to label what it sees, then it turns those labels into lyrics of a song. 7 | 8 | [https://aiexperiments.withgoogle.com/giorgio-cam](https://aiexperiments.withgoogle.com/giorgio-cam) 9 | 10 | This is not an official Google product. 11 | 12 | ## Credits 13 | 14 | Built by [Eric Rosenbaum](https://github.com/ericrosenbaum), [Yotam Mann](https://github.com/tambien), and friends at Google Creative Lab using [MaryTTS](https://github.com/marytts/marytts), [Tone.js](https://github.com/Tonejs/Tone.js), and [Google Cloud Vision API](https://cloud.google.com/vision/). Check out more at [A.I. Experiments] (https://aiexperiments.withgoogle.com). 15 | 16 | ## Overview 17 | 18 | The client-side javascript application captures images using WebRTC. When the user hits the shutter button, an image is sent to the server which then returns an array of labels and confidence scores for that image using Cloud Vision. These labels are dropped into a rhyming template to create the next phrase that the computer will speak. To get the audio of that phrase, the client makes another request to the MaryTTS (text to speech) server which returns a wav file of the audio. That audio is then synced to the music using Tone.js. 19 | 20 | ## FRONT-END 21 | 22 | To build the client-side javascript, first install [node](https://nodejs.org) and [webpack](https://webpack.github.io/). Then you can install of the dependencies of the project by typing the following in the terminal: 23 | 24 | ```bash 25 | cd static 26 | npm install 27 | ``` 28 | 29 | Then build all of the files 30 | 31 | ```bash 32 | webpack -p 33 | ``` 34 | 35 | ## Back-end 36 | 37 | The back-end uses [Google App Engine](https://cloud.google.com/appengine/) to serve static content and mediate between the two other back-end services: Google Cloud Vision and MaryTTS. 38 | 39 | #### Google Cloud Vision API 40 | 41 | You will need to first enable the API and [generate credentials](https://cloud.google.com/vision/docs/common/auth). Under "Key type", use "JSON" and then download the key.json file. 42 | 43 | Add your json key to the `env_variables` section of your `.yaml` file like so: 44 | 45 | ```yaml 46 | env_variables: 47 | GOOGLE_APPLICATION_CREDENTIALS: PATH/TO/CLOUD_VISION_KEY.json 48 | ``` 49 | 50 | #### MaryTTS 51 | 52 | Download and install [MaryTTS](https://github.com/marytts/marytts). Then run the MaryTTS Server. Add the IP Address and Port number that MaryTTS is running on to `server/mary.py`. The default location is `http://localhost:59125` 53 | 54 | ```python 55 | MARY_TTS_URL = 'http://127.0.0.1' 56 | MARY_TTS_PORT = '59125' 57 | ``` 58 | 59 | #### App Engine 60 | 61 | To install the dependencies so they can be launched within Google App Engine they will need to be installed into a local folder (documented [here](https://cloud.google.com/appengine/docs/python/tools/using-libraries-python-27)). 62 | 63 | ```bash 64 | cd server 65 | mkdir lib 66 | pip install -t lib -r requirements.txt 67 | ``` 68 | 69 | Then [follow instructions](https://cloud.google.com/appengine/docs/python/quickstart) on launching your App Engine code. 70 | 71 | ## Music 72 | 73 | [Racer](https://www.youtube.com/watch?v=YT0k99hCY5I) by [Giorgio Moroder](https://en.wikipedia.org/wiki/Giorgio_Moroder) 74 | 75 | 76 | ## License 77 | 78 | Copyright 2016 Google Inc. 79 | 80 | Licensed under the Apache License, Version 2.0 (the "License"); 81 | you may not use this file except in compliance with the License. 82 | You may obtain a copy of the License at 83 | 84 | http://www.apache.org/licenses/LICENSE-2.0 85 | 86 | Unless required by applicable law or agreed to in writing, software 87 | distributed under the License is distributed on an "AS IS" BASIS, 88 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 89 | See the License for the specific language governing permissions and 90 | limitations under the License. 91 | -------------------------------------------------------------------------------- /static/src/interface/Title.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import 'style/title.css' 18 | 19 | const images = { 20 | rest : 'giorgio_logo_anim__0000_eyes_right.png', 21 | blink : 'giorgio_logo_anim__0001_eyes_right_blink.png', 22 | sing0 : 'giorgio_logo_anim__0002_mouth_open.png', 23 | sing1 : 'giorgio_logo_anim__0004_little_note_2.png', 24 | sing2 : 'giorgio_logo_anim__0005_little_note_3.png', 25 | sing3 : 'giorgio_logo_anim__0006_little_note_4.png', 26 | sing4 : 'giorgio_logo_anim__0007_little_note_5.png', 27 | sing5 : 'giorgio_logo_anim__0008_little_note_6.png', 28 | sing6 : 'giorgio_logo_anim__0009_big_notes_1.png', 29 | sing7 : 'giorgio_logo_anim__0010_big_notes_2.png', 30 | sing8 : 'giorgio_logo_anim__0011_big_notes_3.png', 31 | sing9 : 'giorgio_logo_anim__0012_big_notes_4.png', 32 | sing10 : 'giorgio_logo_anim__0013_big_notes_5.png' 33 | } 34 | 35 | export default class Title { 36 | constructor(container){ 37 | 38 | const titleContainer = document.createElement('div') 39 | titleContainer.id = "title" 40 | container.appendChild(titleContainer) 41 | 42 | const giorgi = document.createElement('div') 43 | giorgi.id = "giorgi" 44 | titleContainer.appendChild(giorgi) 45 | 46 | this._face = document.createElement('div') 47 | this._face.id='face' 48 | titleContainer.appendChild(this._face) 49 | 50 | this._faceImg = document.createElement('img') 51 | 52 | const cam = document.createElement('div') 53 | cam.id='cam' 54 | titleContainer.appendChild(cam) 55 | 56 | this._actionIndex = 0 57 | 58 | this._actions = ['_blink', '_sing', '_pause', '_blink', '_pause'] 59 | 60 | // load all the images, then start the animation 61 | this._load().then(this._next.bind(this)) 62 | } 63 | 64 | _load(){ 65 | const promises = [] 66 | Object.keys(images).forEach((key) => { 67 | promises.push(new Promise((success) => { 68 | let img = new Image() 69 | img.onload = success 70 | img.src = `images/face/${images[key]}` 71 | images[key] = img 72 | })) 73 | }) 74 | return Promise.all(promises) 75 | } 76 | 77 | _setImage(img){ 78 | this._faceImg.remove() 79 | this._face.appendChild(img) 80 | this._faceImg = img 81 | // this._face.style.backgroundImage = `url('../images/face/${img}')` 82 | } 83 | 84 | _blink(){ 85 | 86 | this._setImage(images.rest) 87 | 88 | setTimeout(() => { 89 | this._setImage(images.blink) 90 | }, 150) 91 | 92 | setTimeout(() => { 93 | this._setImage(images.rest) 94 | }, 300) 95 | 96 | return new Promise((success) => { 97 | setTimeout(() => { 98 | success() 99 | }, 1000) 100 | }) 101 | } 102 | 103 | _sing(){ 104 | 105 | let wait = 0 106 | for (let i = 0; i <= 10; i++){ 107 | setTimeout(() => { 108 | this._setImage(images[`sing${i}`]) 109 | }, wait) 110 | wait += 150 111 | } 112 | 113 | setTimeout(() => { 114 | this._setImage(images.sing0) 115 | }, wait) 116 | wait += 300 117 | 118 | setTimeout(() => { 119 | this._setImage(images.rest) 120 | }, wait) 121 | wait += 800 122 | 123 | return new Promise((success) => { 124 | setTimeout(() => { 125 | success() 126 | }, wait) 127 | }) 128 | } 129 | 130 | _pause(){ 131 | return new Promise((success) => { 132 | setTimeout(() => { 133 | success() 134 | }, Math.random() * 1200 + 400) 135 | }) 136 | } 137 | 138 | _next(){ 139 | let action = this._actions[this._actionIndex] 140 | this._actionIndex = (this._actionIndex + 1) % this._actions.length 141 | this[action]().then(this._next.bind(this)) 142 | } 143 | } -------------------------------------------------------------------------------- /static/src/voice/Text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import 'style/text.css' 18 | import Tone from 'Tone/core/Tone' 19 | 20 | const textContainer = document.createElement('div') 21 | textContainer.id = 'textContainer' 22 | 23 | const timeline = [] 24 | 25 | function updateLoop(){ 26 | requestAnimationFrame(updateLoop) 27 | let now = Tone.now() 28 | while (timeline.length && timeline[0].time <= now){ 29 | timeline.shift().callback() 30 | } 31 | } 32 | 33 | function makeVisibleCallback(element){ 34 | return () => { 35 | element.classList.add('visible') 36 | } 37 | } 38 | 39 | let shiftUp = false 40 | 41 | export default { 42 | 43 | appendTo : function(container){ 44 | container.appendChild(textContainer) 45 | //start the loop 46 | updateLoop() 47 | }, 48 | 49 | shiftUp : function(){ 50 | shiftUp = true 51 | }, 52 | 53 | shiftDown : function(){ 54 | shiftUp = false 55 | }, 56 | 57 | show : function(text, start, duration, labelObj){ 58 | const words = text.split(/[ ,]+/) 59 | let wordTime = duration / words.length 60 | 61 | let utterance = document.createElement('div') 62 | utterance.classList.add('utterance') 63 | textContainer.appendChild(utterance) 64 | 65 | if (labelObj){ 66 | const splitLabel = labelObj.label.split(/[ ,]+/) 67 | // get the index of the first word of the label 68 | let index = words.indexOf(splitLabel[0]) 69 | 70 | labelObj.count = splitLabel.length 71 | labelObj.split = splitLabel 72 | 73 | // cut the words out and replace it with the label 74 | words.splice(index, splitLabel.length, labelObj.label) 75 | } 76 | 77 | if (shiftUp){ 78 | utterance.classList.add('shiftUp') 79 | } 80 | 81 | 82 | timeline.push({ 83 | time : start, 84 | callback : makeVisibleCallback(utterance) 85 | }) 86 | 87 | let wordDelay = start 88 | 89 | for (let i = 0; i < words.length; i++){ 90 | let word = words[i] 91 | 92 | let element = document.createElement('span') 93 | let wordCount = 1 94 | 95 | if (labelObj && word === labelObj.label){ 96 | // element.textContent = labelObj.label 97 | element.classList.add('label') 98 | 99 | timeline.push({ 100 | time : wordDelay, 101 | callback : makeVisibleCallback(element) 102 | }) 103 | 104 | for (let lWord of labelObj.split){ 105 | let subEl = document.createElement('span') 106 | subEl.textContent = lWord 107 | element.appendChild(subEl) 108 | 109 | timeline.push({ 110 | time : wordDelay, 111 | callback : makeVisibleCallback(subEl) 112 | }) 113 | 114 | wordDelay += wordTime 115 | } 116 | 117 | // add the score 118 | let scoreEl = document.createElement('div') 119 | scoreEl.classList.add('score') 120 | scoreEl.textContent = `${(labelObj.score * 100).toFixed()}% confidence` 121 | element.appendChild(scoreEl) 122 | 123 | timeline.push({ 124 | time : wordDelay - wordTime, 125 | callback : makeVisibleCallback(scoreEl) 126 | }) 127 | 128 | } else { 129 | element.textContent = words[i] 130 | timeline.push({ 131 | time : wordDelay, 132 | callback : makeVisibleCallback(element) 133 | }) 134 | wordDelay += wordTime 135 | } 136 | utterance.appendChild(element) 137 | 138 | } 139 | 140 | // remove it at the end 141 | timeline.push({ 142 | time : start + duration + (labelObj ? 1.4 : 0.3), 143 | callback : () => { 144 | utterance.remove() 145 | } 146 | }) 147 | 148 | updateLoop() 149 | }, 150 | 151 | clear : function(){ 152 | // textContainer.remove() 153 | //clear the text events 154 | textContainer.innerHTML = '' 155 | //flush all the events 156 | while(timeline.length){ 157 | timeline.shift() 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /static/third_party/Modernizr.js: -------------------------------------------------------------------------------- 1 | /*! modernizr 3.3.1 (Custom Build) | MIT * 2 | * https://modernizr.com/download/?-getusermedia-webaudio-webgl !*/ 3 | !function(e,n,t){function r(e,n){return typeof e===n}function o(){var e,n,t,o,i,s,a;for(var u in g)if(g.hasOwnProperty(u)){if(e=[],n=g[u],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;td;d++)if(v=e[d],g=T.style[v],a(v,"-")&&(v=i(v)),T.style[v]!==t){if(u||r(o,"undefined"))return f(),"pfx"==n?v:!0;try{T.style[v]=o}catch(y){}if(T.style[v]!=g)return f(),"pfx"==n?v:!0}return f(),!1}function v(e,n,t,o,i){var s=e.charAt(0).toUpperCase()+e.slice(1),a=(e+" "+S.join(s+" ")+s).split(" ");return r(n,"string")||r(n,"undefined")?m(a,n,o,i):(a=(e+" "+b.join(s+" ")+s).split(" "),f(a,n,t))}var g=[],h={_version:"3.3.1",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var t=this;setTimeout(function(){n(t[e])},0)},addTest:function(e,n,t){g.push({name:e,fn:n,options:t})},addAsyncTest:function(e){g.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=h,Modernizr=new Modernizr,Modernizr.addTest("webaudio",function(){var n="webkitAudioContext"in e,t="AudioContext"in e;return Modernizr._config.usePrefixes?n||t:t});var y=[],C=n.documentElement,x="svg"===C.nodeName.toLowerCase();Modernizr.addTest("webgl",function(){var n=s("canvas"),t="probablySupportsContext"in n?"probablySupportsContext":"supportsContext";return t in n?n[t]("webgl")||n[t]("experimental-webgl"):"WebGLRenderingContext"in e});var w="Moz O ms Webkit",S=h._config.usePrefixes?w.split(" "):[];h._cssomPrefixes=S;var _=function(n){var r,o=prefixes.length,i=e.CSSRule;if("undefined"==typeof i)return t;if(!n)return!1;if(n=n.replace(/^@/,""),r=n.replace(/-/g,"_").toUpperCase()+"_RULE",r in i)return"@"+n;for(var s=0;o>s;s++){var a=prefixes[s],u=a.toUpperCase()+"_"+r;if(u in i)return"@-"+a.toLowerCase()+"-"+n}return!1};h.atRule=_;var b=h._config.usePrefixes?w.toLowerCase().split(" "):[];h._domPrefixes=b;var E={elem:s("modernizr")};Modernizr._q.push(function(){delete E.elem});var T={style:E.elem.style};Modernizr._q.unshift(function(){delete T.style}),h.testAllProps=v;var z=h.prefixed=function(e,n,t){return 0===e.indexOf("@")?_(e):(-1!=e.indexOf("-")&&(e=i(e)),n?v(e,n,t):v(e,"pfx"))};Modernizr.addTest("getusermedia",!!z("getUserMedia",navigator)),o(),delete h.addTest,delete h.addAsyncTest;for(var P=0;P, 4 | licensed under the SIL OFL 1.1 . (C) 2009-2016. 5 | 6 | ----------------------------------------------------------- 7 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 8 | ----------------------------------------------------------- 9 | 10 | PREAMBLE 11 | The goals of the Open Font License (OFL) are to stimulate worldwide 12 | development of collaborative font projects, to support the font creation 13 | efforts of academic and linguistic communities, and to provide a free and 14 | open framework in which fonts may be shared and improved in partnership 15 | with others. 16 | 17 | The OFL allows the licensed fonts to be used, studied, modified and 18 | redistributed freely as long as they are not sold by themselves. The 19 | fonts, including any derivative works, can be bundled, embedded, 20 | redistributed and/or sold with any software provided that any reserved 21 | names are not used by derivative works. The fonts and derivatives, 22 | however, cannot be released under any other type of license. The 23 | requirement for fonts to remain under this license does not apply 24 | to any document created using the fonts or their derivatives. 25 | 26 | DEFINITIONS 27 | "Font Software" refers to the set of files released by the Copyright 28 | Holder(s) under this license and clearly marked as such. This may 29 | include source files, build scripts and documentation. 30 | 31 | "Reserved Font Name" refers to any names specified as such after the 32 | copyright statement(s). 33 | 34 | "Original Version" refers to the collection of Font Software components as 35 | distributed by the Copyright Holder(s). 36 | 37 | "Modified Version" refers to any derivative made by adding to, deleting, 38 | or substituting -- in part or in whole -- any of the components of the 39 | Original Version, by changing formats or by porting the Font Software to a 40 | new environment. 41 | 42 | "Author" refers to any designer, engineer, programmer, technical 43 | writer or other person who contributed to the Font Software. 44 | 45 | PERMISSION & CONDITIONS 46 | Permission is hereby granted, free of charge, to any person obtaining 47 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 48 | redistribute, and sell modified and unmodified copies of the Font 49 | Software, subject to the following conditions: 50 | 51 | 1) Neither the Font Software nor any of its individual components, 52 | in Original or Modified Versions, may be sold by itself. 53 | 54 | 2) Original or Modified Versions of the Font Software may be bundled, 55 | redistributed and/or sold with any software, provided that each copy 56 | contains the above copyright notice and this license. These can be 57 | included either as stand-alone text files, human-readable headers or 58 | in the appropriate machine-readable metadata fields within text or 59 | binary files as long as those fields can be easily viewed by the user. 60 | 61 | 3) No Modified Version of the Font Software may use the Reserved Font 62 | Name(s) unless explicit written permission is granted by the corresponding 63 | Copyright Holder. This restriction only applies to the primary font name as 64 | presented to the users. 65 | 66 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 67 | Software shall not be used to promote, endorse or advertise any 68 | Modified Version, except to acknowledge the contribution(s) of the 69 | Copyright Holder(s) and the Author(s) or with their explicit written 70 | permission. 71 | 72 | 5) The Font Software, modified or unmodified, in part or in whole, 73 | must be distributed entirely under this license, and must not be 74 | distributed under any other license. The requirement for fonts to 75 | remain under this license does not apply to any document created 76 | using the Font Software. 77 | 78 | TERMINATION 79 | This license becomes null and void if any of the above conditions are 80 | not met. 81 | 82 | DISCLAIMER 83 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 84 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 85 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 86 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 87 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 88 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 89 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 90 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 91 | OTHER DEALINGS IN THE FONT SOFTWARE. 92 | -------------------------------------------------------------------------------- /static/src/camera/Image.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | import Time from 'Tone/type/Time' 18 | import knuthShuffle from 'knuth-shuffle' 19 | const shuffle = knuthShuffle.knuthShuffle 20 | 21 | export default class Img { 22 | constructor(container){ 23 | 24 | this._canvas = document.createElement('canvas') 25 | this._canvas.id = 'image' 26 | container.appendChild(this._canvas) 27 | 28 | this._context = this._canvas.getContext('2d') 29 | 30 | this._updateInterval = Time('4n').toMilliseconds(); 31 | 32 | this._lastUpdate = Date.now() 33 | 34 | this._currentImg = null 35 | 36 | this._isVisible = false 37 | 38 | this._colorIndex = 0 39 | this._colors = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 0, 255], [255, 255, 0], [0, 255, 255]] 40 | // these colors are from JR: 41 | // this._colors = [[7,222,0],[9,51,255],[93,17,255],[101,177,255],[255,42,2],[255,181,115],[250,196,0],[5,153,0],[255,251,83],[46,220,0],[246,168,0],[182,87,255],[194,208,0]] 42 | 43 | this.resize() 44 | this._loop() 45 | } 46 | 47 | _loop(){ 48 | requestAnimationFrame(this._loop.bind(this)) 49 | if (this._isVisible && this._currentImg && Date.now() - this._lastUpdate > this._updateInterval){ 50 | this._lastUpdate = Date.now() 51 | this._drawImg(this._currentImg) 52 | } 53 | } 54 | 55 | bounce(){ 56 | this._canvas.classList.add('bounce') 57 | } 58 | 59 | show(){ 60 | this._lastUpdate = 0 61 | this._canvas.classList.add('visible') 62 | this._isVisible = true 63 | } 64 | 65 | hide(){ 66 | this._isVisible = false 67 | this._canvas.classList.remove('visible') 68 | this._canvas.classList.remove('bounce') 69 | this._currentImg = null 70 | } 71 | 72 | setImage(img){ 73 | this._currentImg = img 74 | } 75 | 76 | resize(){ 77 | this._context.canvas.width = window.innerWidth 78 | this._context.canvas.height = window.innerHeight 79 | } 80 | 81 | _grayScale(context){ 82 | 83 | let color = this._colors[this._colorIndex] 84 | this._colorIndex = (this._colorIndex + 1) % this._colors.length 85 | 86 | let width = context.canvas.width 87 | let height = context.canvas.height 88 | 89 | let pixels = context.getImageData(0, 0, width, height); 90 | 91 | for(let y = 0; y < pixels.height; y++){ 92 | for(let x = 0; x < pixels.width; x++){ 93 | let i = (y * 4) * pixels.width + x * 4; 94 | let avg = (pixels.data[i] + pixels.data[i + 1] + pixels.data[i + 2]) / 3; 95 | pixels.data[i] = avg - (255 - color[0]); 96 | pixels.data[i + 1] = avg - (255 - color[1]); 97 | pixels.data[i + 2] = avg - (255 - color[2]); 98 | } 99 | } 100 | 101 | context.putImageData(pixels, 0, 0, 0, 0, pixels.width, pixels.height); 102 | } 103 | 104 | _drawImg(image){ 105 | let context = this._context 106 | let imageW = image.width 107 | let imageH = image.height 108 | let canvasW = context.canvas.width 109 | let canvasH = context.canvas.height 110 | let canvasAspectRatio = canvasW / canvasH 111 | let imageAspectRatio = imageW / imageH 112 | // get the smaller of the frame 113 | if (canvasAspectRatio > imageAspectRatio){ 114 | //fit width & scale height 115 | let widthScale = imageW / canvasW 116 | let scaleHeight = imageH / widthScale 117 | // the diff between the canvas width and the image width 118 | let topOffset = (scaleHeight - canvasH) / 2 119 | context.drawImage(image, 0, -topOffset, canvasW, scaleHeight) 120 | } else { 121 | //fit height & scale width 122 | let heightScale = imageH / canvasH 123 | let scaledWidth = imageW / heightScale 124 | // the diff between the canvas width and the image width 125 | let leftOffset = (scaledWidth - canvasW) / 2 126 | context.drawImage(image, -leftOffset, 0, scaledWidth, canvasH) 127 | } 128 | this._grayScale(context) 129 | } 130 | 131 | draw(context){ 132 | if (this._currentImg && Date.now() - this._lastUpdate > this._updateInterval){ 133 | this._lastUpdate = Date.now() 134 | this._drawImg(this._currentImg, context) 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /static/images/badgeAI_master.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 12 | 13 | 15 | 17 | 19 | 22 | 25 | 27 | 29 | 32 | 35 | 37 | 39 | 41 | 43 | 45 | 48 | 50 | 53 | 56 | 58 | 59 | -------------------------------------------------------------------------------- /static/src/camera/Canvas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | const THREE = require('three') 18 | import Time from 'Tone/type/Time' 19 | import TWEEN from 'tween.js' 20 | 21 | const CAMERA_DIST = 10 22 | 23 | const TWO_PI = Math.PI * 2 24 | 25 | export default class Canvas { 26 | constructor(container){ 27 | 28 | this._camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 10000 ) 29 | this._camera.position.z = CAMERA_DIST 30 | 31 | 32 | this._scene = new THREE.Scene() 33 | 34 | this._renderer = new THREE.WebGLRenderer({ antialias: false }) 35 | container.appendChild(this._renderer.domElement) 36 | this._renderer.domElement.id = 'three' 37 | 38 | // make a test plane 39 | const material = new THREE.MeshBasicMaterial({ color : 0xffffff }) 40 | // material.side = THREE.DoubleSide 41 | this._plane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material) 42 | 43 | window.plane = this._plane 44 | 45 | this._scene.add(this._plane) 46 | 47 | this._bouncing = false 48 | 49 | this._bounceStart = 0 50 | 51 | this._bounceRate = Time('4n').toMilliseconds() 52 | 53 | this._changingColor = false 54 | 55 | this._colorChangeTime = 0 56 | 57 | this._colorChangeIndex = 0 58 | 59 | this._colors = [[7,222,0],[9,51,255],[93,17,255],[101,177,255],[255,42,2],[255,181,115],[250,196,0],[5,153,0],[255,251,83],[46,220,0],[246,168,0],[182,87,255],[194,208,0]] 60 | .map((rgb) => new THREE.Color(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255)) 61 | 62 | this._loop() 63 | } 64 | 65 | _loop(time){ 66 | requestAnimationFrame(this._loop.bind(this)) 67 | 68 | let now = Date.now() 69 | 70 | if (this._bouncing){ 71 | // let bounceAmount = Math.sin(this._bounceRate * TWO_PI * (now - this._bounceStart) / 1000) 72 | // bounceAmount = (bounceAmount + 1) 73 | let bounceAmount = (now - this._bounceStart) % this._bounceRate 74 | bounceAmount /= this._bounceRate 75 | this._camera.position.z = CAMERA_DIST - (1 - bounceAmount) / 2 76 | } 77 | 78 | if (this._changingColor && ((now - this._colorChangeTime) > (this._bounceRate))){ 79 | this._plane.material.color = this._colors[this._colorChangeIndex] 80 | this._colorChangeIndex = (this._colorChangeIndex + 1) % this._colors.length 81 | this._colorChangeTime = now 82 | } 83 | 84 | this._renderer.render( this._scene, this._camera ) 85 | 86 | TWEEN.update(time); 87 | } 88 | 89 | changeColor(){ 90 | this._changingColor = true 91 | this._colorChangeTime = Date.now() 92 | } 93 | 94 | repeatTexture(){ 95 | const texture = this._plane.material.map 96 | window.texture = texture 97 | texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping; 98 | const repeats = [1, 2, 4, 8] 99 | let index = 0 100 | this._interval = setInterval(() => { 101 | texture.repeat.x = repeats[index] 102 | texture.repeat.y = repeats[index] 103 | index = (index + 1) % repeats.length 104 | }, this._bounceRate) 105 | } 106 | 107 | endChangeColor(){ 108 | this._changingColor = false 109 | this._plane.material.color = new THREE.Color(1, 1, 1) 110 | // clearInterval(this._interval) 111 | // this._plane.material.map.repeat.x = 1 112 | // this._plane.material.map.repeat.y = 1 113 | } 114 | 115 | /** 116 | * mirror the display for front facing cameras 117 | */ 118 | mirror(){ 119 | this._plane.scale.x *= -1 120 | this._plane.material.side = THREE.BackSide 121 | this._plane.material.needsUpdate = true 122 | } 123 | 124 | resize(vidWidth, vidHeight){ 125 | 126 | 127 | 128 | //set the video plane size 129 | const vidAspect = vidWidth / vidHeight 130 | const camAspect = window.innerWidth / window.innerHeight 131 | 132 | this._plane.scale.x = vidAspect 133 | 134 | //http://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object 135 | if (vidAspect > camAspect){ 136 | this._camera.fov = 2 * Math.atan( 1 / ( 2 * CAMERA_DIST ) ) * ( 180 / Math.PI ) 137 | } else { 138 | this._camera.fov = 2 * Math.atan( ( vidAspect / camAspect ) / ( 2 * CAMERA_DIST ) ) * ( 180 / Math.PI ) 139 | } 140 | 141 | //THREE resize 142 | this._renderer.setPixelRatio( window.devicePixelRatio ) 143 | this._renderer.setSize( window.innerWidth, window.innerHeight ) 144 | 145 | const windowHalfX = window.innerWidth / 2 146 | const windowHalfY = window.innerHeight / 2 147 | this._camera.aspect = camAspect 148 | this._camera.updateProjectionMatrix() 149 | this._renderer.setSize( window.innerWidth, window.innerHeight ) 150 | } 151 | 152 | setVideo(video){ 153 | 154 | this._renderer.domElement.classList.add('visible') 155 | 156 | const texture = new THREE.VideoTexture(video) 157 | texture.minFilter = THREE.LinearFilter; 158 | texture.magFilter = THREE.LinearFilter; 159 | this._plane.material.map = texture 160 | this._plane.material.needsUpdate = true 161 | } 162 | 163 | bounce(){ 164 | this._bouncing = true 165 | this._bounceStart = Date.now() 166 | } 167 | 168 | endBounce(){ 169 | this._bouncing = false 170 | } 171 | 172 | fail(time = 4000){ 173 | this._bouncing = false 174 | return new Promise((end) => { 175 | let plane = this._plane 176 | let scene = this._scene 177 | var tween = new TWEEN.Tween({scale : 1}) 178 | .to({scale : 0}, time) 179 | .onUpdate(function(){ 180 | plane.scale.y = this.scale 181 | }) 182 | .onComplete(function(){ 183 | scene.remove(plane) 184 | end() 185 | }) 186 | .easing(TWEEN.Easing.Quadratic.Out) 187 | .start() 188 | }) 189 | } 190 | } -------------------------------------------------------------------------------- /static/style/splash.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. 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 | */ 16 | 17 | @import 'common.css'; 18 | 19 | $topMargin: 40px; 20 | 21 | #splash { 22 | font-weight: bold; 23 | position: absolute; 24 | width: 100%; 25 | height: 100%; 26 | top: 0px; 27 | left: 0px; 28 | background-color: black; 29 | color: white; 30 | 31 | transition: opacity 0.1s; 32 | 33 | &.disappear { 34 | opacity: 0; 35 | pointer-events: none; 36 | } 37 | 38 | * { 39 | line-height: 30px; 40 | // font-size: 16px; 41 | font-size: 20px; 42 | width: 80%; 43 | margin-left: auto; 44 | margin-right: auto; 45 | text-align: center; 46 | } 47 | 48 | #learnMore { 49 | margin-top: $topMargin; 50 | font-size: 18px; 51 | display: block; 52 | width: 100px; 53 | @mixin yellowLink; 54 | } 55 | 56 | #titleContainer { 57 | position: absolute; 58 | top: 50%; 59 | left: 50%; 60 | width: 80%; 61 | transform: translate(-50%, -50%); 62 | text-align: center; 63 | min-width: 300px; 64 | 65 | $pink : rgb(255, 82, 184); 66 | $purple : rgb(115, 58, 236); 67 | $yellow : rgb(255, 241, 6); 68 | $teal : rgb(10, 247, 247); 69 | 70 | #title { 71 | line-height: 60px; 72 | font-size: 50px; 73 | letter-spacing: 1px; 74 | text-transform: uppercase; 75 | 76 | span { 77 | line-height: 60px; 78 | font-size: 50px; 79 | margin-right: 1px; 80 | 81 | &.pink { 82 | color: $pink; 83 | } 84 | 85 | &.purple { 86 | color: $purple; 87 | } 88 | 89 | &.teal { 90 | color: $teal; 91 | } 92 | 93 | &.yellow { 94 | color: $yellow; 95 | } 96 | } 97 | } 98 | 99 | #subTitle { 100 | margin-top: $topMargin; 101 | letter-spacing: 0.8px; 102 | line-height: 30px; 103 | font-size: 22px; 104 | width: 80%; 105 | margin-left: auto; 106 | margin-right: auto; 107 | text-align: center; 108 | text-transform: uppercase; 109 | } 110 | 111 | $loaderWidth: 160px; 112 | $loaderHeight: 55px; 113 | 114 | #loader { 115 | position: relative; 116 | margin-top: $topMargin; 117 | background-color: black; 118 | border: 3px solid white; 119 | width: $loaderWidth; 120 | height: $loaderHeight; 121 | margin-left: auto; 122 | margin-right: auto; 123 | text-transform: uppercase; 124 | overflow: hidden; 125 | 126 | &.clickable { 127 | cursor: pointer; 128 | 129 | transition: transform 0.1s; 130 | 131 | &:hover { 132 | transform: scale(1.1); 133 | } 134 | 135 | #fillText:active { 136 | color: black!important; 137 | background-color: white; 138 | } 139 | } 140 | 141 | #loaderText { 142 | position: absolute; 143 | width: 100%; 144 | height: 100%; 145 | color: black; 146 | background-color: white; 147 | } 148 | 149 | #fill { 150 | position: absolute; 151 | height: 100%; 152 | width: 0%; 153 | overflow: hidden; 154 | background-color: black; 155 | 156 | #fillText { 157 | width: $loaderWidth; 158 | height: 100%; 159 | color: white; 160 | 161 | &:before{ 162 | background-color: white; 163 | } 164 | } 165 | } 166 | 167 | #loaderText, #fillText { 168 | line-height: $loaderHeight; 169 | font-size: 22px; 170 | text-align: center; 171 | font-weight: normal; 172 | padding-left: 18px; 173 | 174 | &:before { 175 | content : ' '; 176 | mask-image : url(../images/camera_icon.svg); 177 | position: absolute; 178 | left: 22px; 179 | top: 17px; 180 | width: 25px; 181 | height: 25px; 182 | background-repeat: no-repeat; 183 | mask-repeat: no-repeat; 184 | background-color: black; 185 | } 186 | } 187 | } 188 | 189 | } 190 | 191 | #buildWith { 192 | margin-top: $topMargin; 193 | } 194 | 195 | $badgeWidth : 80px; 196 | $badgeHeight: 50px; 197 | $badgeMargin : 20px; 198 | $badegOpacity: 0.7; 199 | 200 | #aiExperiments, #googleFriends { 201 | width: $badgeWidth; 202 | height: $badgeHeight; 203 | position: absolute; 204 | bottom: $badgeMargin; 205 | opacity: $badegOpacity; 206 | background-repeat: no-repeat; 207 | background-size: 100% 100%; 208 | } 209 | 210 | 211 | #aiExperiments { 212 | left: $badgeMargin; 213 | background-image: url(../images/badgeAI_master.svg); 214 | 215 | &:hover { 216 | opacity: 1; 217 | 218 | &:active { 219 | opacity: 0.3; 220 | } 221 | } 222 | } 223 | 224 | #googleFriends { 225 | cursor: initial; 226 | left: calc($badgeWidth + $badgeMargin * 3); 227 | background-image: url(../images/badgeFriends_master.svg); 228 | } 229 | 230 | #badgeBreak{ 231 | $breakHeight: calc($badgeHeight * 0.8); 232 | height: $breakHeight; 233 | left: calc($badgeWidth + $badgeMargin * 1.8); 234 | background-color: white; 235 | opacity: calc($badegOpacity / 2); 236 | position: absolute; 237 | width: 1px; 238 | bottom: calc($badgeMargin + ($badgeHeight - $breakHeight) / 2); 239 | } 240 | 241 | #privacyAndTerms { 242 | position: absolute; 243 | bottom: $badgeMargin; 244 | right: $badgeMargin; 245 | width: auto; 246 | 247 | * { 248 | height: 14px; 249 | line-height: 14px; 250 | font-size: 14px; 251 | color: white; 252 | display: inline; 253 | opacity: $badegOpacity; 254 | margin: 2px; 255 | } 256 | 257 | a { 258 | text-decoration: none; 259 | cursor: pointer; 260 | 261 | &:hover { 262 | opacity: 1; 263 | 264 | &:active { 265 | opacity: 0.3; 266 | } 267 | } 268 | } 269 | } 270 | } -------------------------------------------------------------------------------- /static/images/badgeFriends_master.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 13 | 16 | 19 | 21 | 23 | 25 | 27 | 30 | 33 | 36 | 39 | 41 | 43 | 45 | 48 | 50 | 53 | 56 | 58 | 60 | 63 | 66 | 69 | 72 | 76 | 77 | 80 | 83 | 84 | --------------------------------------------------------------------------------