Spaces:
Build error
Build error
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// https://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
import $ from 'jquery' | |
import 'jquery.cookie'; | |
import 'bootstrap' | |
import FileSaver from 'file-saver'; | |
// Number of classes to classify | |
const NUM_CLASSES = 8; | |
String.prototype.sprintf = function() | |
{ | |
let str = this + ''; | |
const args = Array.prototype.slice.call(arguments); | |
let ph = true; | |
if (str.indexOf('%s', 0) != -1) { | |
ph = false; | |
} | |
if (args.length === 1) { | |
if (ph) { | |
return str.replace(/%1$s/g, args[0]); | |
} else { | |
return str.replace(/%s/g, args[0]); | |
} | |
} else { | |
for (let i=0; i<args.length; i++) { | |
const n = i + 1; | |
if (ph) { | |
str = str.replace('%'+n+'$s', args[i]); | |
} else { | |
str = str.replace('%s', args[i]); | |
} | |
} | |
} | |
return str; | |
} | |
const LOCALIZED_TEXT = { | |
ja: { | |
input: "入力", | |
connection: "接続", | |
trained_model: "学習済みモデル", | |
trained_model_text: "学習済みのモデルをアップロードして、これまで学習したモデルと入れ替えます。", | |
training: "学習", | |
trained_images: "学習済み画像", | |
settings: "設定", | |
settings_help_text: "WebSocketサーバーのURL", | |
connect: "接続", | |
connection_id: "接続ID", | |
recognition: '認識', | |
blank_id_is_invalid: "接続IDを入力してください。", | |
no_examples_added: "まだ学習していません", | |
examples: "枚", | |
train: '「分類%s」として学習する', | |
edit_label: 'ラベルを編集', | |
clear: 'リセット', | |
clear_all: 'すべてをリセット', | |
download: '学習済みモデルをダウンロード', | |
upload: 'アップロード', | |
help_text: "ML2Scratch用の拡張機能が追加されたScratchのページを開いて、下記の接続IDを「ID: [ ]で接続する」ブロックにコピー&ペーストしてください。", | |
open_scratch: 'Scratchを開く', | |
choose_file: 'ファイルを選択...', | |
readme: "README(説明)", | |
readme_url: "https://github.com/champierre/ml2scratch/blob/master/README.ja.md" | |
}, | |
en: { | |
input: "Input", | |
connection: "Connect", | |
trained_model: "Trained Model", | |
trained_model_text: 'Upload trained model.', | |
training: "Training", | |
trained_images: "Trained Images", | |
settings: "Settings", | |
settings_help_text: "WebSocket Server URL", | |
connect: "Connect", | |
connection_id: "Connection ID", | |
recognition: 'Recognition', | |
blank_id_is_invalid: "Blank ID is invalid.", | |
no_examples_added: "No examples added", | |
examples: "examples", | |
train: 'Train %s', | |
edit_label: 'Edit label', | |
clear: 'Reset', | |
clear_all: 'Reset all', | |
download: 'Download trained model', | |
upload: 'Upload', | |
help_text: "Open Scratch with ML2Scratch extension added and use this ID when you connect.", | |
open_scratch: 'Open Scratch', | |
choose_file: 'Choose File...', | |
readme: 'README', | |
readme_url: "https://github.com/champierre/ml2scratch/blob/master/README.md" | |
}, | |
zh_cn: { | |
input: "输入", | |
connection: "连接", | |
trained_model: "学习模型", | |
trained_model_text: "上传学习模型", | |
training: "学习", | |
trained_images: "Trained Images", | |
settings: "设置", | |
settings_help_text: "WebSocket服务器链接", | |
connect: "连接", | |
connection_id: "连接ID", | |
recognition: "识别", | |
blank_id_is_invalid: "Blank ID is invalid.", | |
no_examples_added: "尚未学习", | |
examples: "examples", | |
train: '学习类别 %s', | |
edit_label: '编辑类别', | |
clear: '重置', | |
clear_all: '重置所有类别', | |
download: '下载', | |
upload: '上传', | |
help_text: "打开已加入扩展功能的Scratch的页面,把上面的连接ID拷贝到[用ID: []连接]模块的空白处。", | |
open_scratch: '打开Scratch', | |
choose_file: '选取文件...', | |
readme: 'README', | |
readme_url: "https://github.com/champierre/ml2scratch/blob/master/README.zh-cn.md" | |
} | |
} | |
class I18n { | |
constructor(){ | |
window.I18n = this; | |
$('[data-locale]').each(function(i, el) { | |
$(el).html(I18n.t($(el).data("locale"))); | |
}); | |
} | |
static t(key, arg = '') { | |
let lang = window.navigator.language; | |
const vars = this.getUrlVars(); | |
if (vars['lang'] && vars['lang'].length > 0) { | |
lang = vars['lang']; | |
} | |
if (lang == 'ja') { | |
return LOCALIZED_TEXT['ja'][key].sprintf(arg); | |
} else if (lang == 'zh_cn') { | |
return LOCALIZED_TEXT['zh_cn'][key].sprintf(arg); | |
}else { | |
return LOCALIZED_TEXT['en'][key].sprintf(arg); | |
} | |
} | |
static getUrlVars() { | |
let vars = [], max = 0, hash = "", array = ""; | |
const url = window.location.search; | |
hash = url.slice(1).split('&'); | |
max = hash.length; | |
for (let i = 0; i < max; i++) { | |
array = hash[i].split('='); | |
vars.push(array[0]); | |
vars[array[0]] = array[1]; | |
} | |
return vars; | |
} | |
} | |
class Main { | |
constructor(){ | |
// Initiate variables | |
this.infoTexts = []; | |
this.training = -1; // -1 when no class is being trained | |
this.videoPlaying = false; | |
this.connId = undefined; | |
this.wss_url = $.cookie('wss_url') || "wss://ml2scratch-helper.glitch.me" | |
this.video = $('video')[0]; | |
this.isTouchDevice = 'ontouchstart' in document.documentElement; | |
this.knnClassifier = ml5.KNNClassifier(); | |
this.featureExtractor = ml5.featureExtractor('MobileNet', () => { | |
this.start(); | |
}); | |
let params = new URLSearchParams(window.location.search); | |
if (params.get('conn_id')) { | |
this.connId = params.get('conn_id'); | |
} else { | |
this.connId = Math.floor(Math.random(100000000) * 100000000) | |
} | |
$('#conn-id').val(this.connId); | |
this.connect(this.connId); | |
// Create cards. This needs to be run at the first place. | |
for(let i=0;i<NUM_CLASSES; i++){ | |
this.addCard(); | |
} | |
this.infoTexts = $('#learning .info-text'); | |
this.images = []; | |
for(let i=0;i<NUM_CLASSES; i++){ | |
this.images[i] = []; | |
} | |
// Replace readme href | |
$('#readme').attr("href", I18n.t("readme_url")); | |
$('#trained-images .images').hide(); | |
$('#clear-all-menu').on('click', ()=> { | |
this.clearAll(); | |
return false; | |
}); | |
$('#learning .edit-label-menu').each((i, el) => { | |
$(el).on('click', ()=> { | |
this.editLabel(i); | |
return false; | |
}); | |
}); | |
$('#learning .clear-menu').each((i, el) => { | |
$(el).on('click', ()=> { | |
this.clear(i); | |
return false; | |
}); | |
}); | |
$('#download-button').on('click', ()=> { | |
this.download(); | |
return false; | |
}); | |
$('#conn-id').on('click', (e)=> { | |
$(e.target).select(); | |
}) | |
// fileを選択したら名前を表示する | |
$('[data-file]').each(function(index, el) { | |
$(el).on('change', function(e) { | |
let filename = $(e.currentTarget).val().split('\\').pop() | |
let element = $(e.currentTarget).closest('.input-file').find('[data-file-name]')[0] | |
element.innerText = filename; | |
$(e.currentTarget).closest('.input-file').addClass('has-file') | |
}); | |
}); | |
$("#upload-files").change(()=>{ | |
this.upload(); | |
}); | |
$('.card-block').each((i, el) => { | |
$(el).on('click', ()=> { | |
$('#trained-images .images').hide(); | |
$('#trained-images .images').eq(i).show(); | |
$("#trained-images .training-id").html(i); | |
}); | |
}); | |
// Create training buttons and info texts | |
for(let i=0;i<NUM_CLASSES; i++){ | |
$('#learning .card-block__label').eq(i).html(`${i}`); | |
let button = $('#learning button').eq(i); | |
// Listen for mouse events when clicking the button | |
button.mousedown(()=>{ | |
if (this.isTouchDevice == false) { | |
this.training = i; | |
} | |
}); | |
button.mouseup(()=>{ | |
if (this.isTouchDevice == false) { | |
this.training = -1; | |
} | |
}); | |
button.on('touchstart', ()=>{ | |
if (this.isTouchDevice) { | |
this.training = i; | |
} | |
}); | |
button.on('touchend', ()=>{ | |
if (this.isTouchDevice) { | |
this.training = -1; | |
} | |
}); | |
$('#learning .card-block .input').eq(i).on("blur", ()=>{ | |
let label = $('#learning .card-block .card-block__label').eq(i); | |
let input = $('#learning .card-block .input').eq(i); | |
label.removeClass("none"); | |
input.addClass("none"); | |
label.html(input.val() || i); | |
}) | |
} | |
$('#wss_url').val(this.wss_url); | |
$('#wss_url').on('blur', (e)=> { | |
this.wss_url = $(e.target).val(); | |
$.cookie('wss_url', this.wss_url, { expires: 90 }); | |
}); | |
$('#connect-button').on('click', (e)=> { | |
let connId = $('#conn-id').val(); | |
this.connect(connId); | |
return false; | |
}); | |
$("#scratch-link").attr('href', 'https://champierre.github.io/scratch/'); | |
// Setup webcam | |
navigator.mediaDevices.getUserMedia({video: true, audio: false}) | |
.then((stream) => { | |
this.video.srcObject = stream; | |
this.video.addEventListener('playing', ()=> this.videoPlaying = true); | |
this.video.addEventListener('paused', ()=> this.videoPlaying = false); | |
}) | |
$(window).on('beforeunload', function() { | |
if (location.href != "http://localhost:9966/dist/") { | |
return 'ページから離れようとしていますが、よろしいですか?'; | |
} | |
}); | |
} | |
start() { | |
if (this.timer) { | |
this.stop(); | |
} | |
this.video.play(); | |
this.timer = requestAnimationFrame(this.animate.bind(this)); | |
} | |
stop() { | |
this.video.pause(); | |
cancelAnimationFrame(this.timer); | |
} | |
classify() { | |
const numLabels = this.knnClassifier.getNumLabels(); | |
if (numLabels == 0) return; | |
const features = this.featureExtractor.infer(this.video); | |
this.knnClassifier.classify(features, (err, result) => { | |
if (err) { | |
console.error(err); | |
} else { | |
if(this.ws && this.ws.readyState === WebSocket.OPEN){ | |
let label = $('#learning .card-block .card-block__label').eq(result.classIndex).html(); | |
this.ws.send(JSON.stringify({action: 'predict', conn_id: this.connId, value: result.classIndex, label: label})); | |
} | |
this.updateProgress(result.confidences); | |
} | |
}); | |
} | |
animate() { | |
if(this.videoPlaying){ | |
this.classify(); | |
// Train class if one of the buttons is held down | |
if(this.training != -1){ | |
const features = this.featureExtractor.infer(this.video); | |
this.knnClassifier.addExample(features, String(this.training)); | |
this.updateCounts(); | |
} | |
} | |
this.timer = requestAnimationFrame(this.animate.bind(this)); | |
} | |
connect(connId) { | |
this.ws = new WebSocket(`${this.wss_url}/ml`); | |
this.connId = connId; | |
} | |
download() { | |
const fileName = String(Date.now()); | |
this.knnClassifier.save(fileName); | |
} | |
upload() { | |
const files = document.getElementById('upload-files').files; | |
if (files.length <= 0) { | |
return false; | |
} | |
const fr = new FileReader(); | |
fr.onload = (e) => { | |
const data = JSON.parse(e.target.result); | |
this.knnClassifier.load(data, () => { | |
this.updateCounts(); | |
}); | |
} | |
fr.onloadend = (e) => { | |
document.getElementById('upload-files').value = ""; | |
} | |
fr.readAsText(files.item(0)); | |
} | |
editLabel(i) { | |
let label = $('.card-block .card-block__label').eq(i); | |
let input = $('.card-block .input').eq(i); | |
label.addClass('none'); | |
input.removeClass('none'); | |
input.val(label.html()); | |
} | |
clear(i) { | |
this.knnClassifier.clearLabel(String(i)); | |
this.updateCounts(); | |
} | |
clearAll() { | |
this.knnClassifier.clearAllLabels() | |
this.updateCounts(); | |
} | |
updateProgress(confidences) { | |
let html = ''; | |
let labels = $('.card-block .card-block__label'); | |
$.each(confidences, function(i, confidence) { | |
let label = ""; | |
if (confidence > 0) { | |
label = labels.eq(i).html(); | |
} | |
html += `<div class="bar" style="flex-basis: ${confidence * 100}%"><div class="label">${label}</div></div>`; | |
}); | |
$('.progress').html(html); | |
} | |
addCard() { | |
const html = ` | |
<!-- card-block --> | |
<div class="card-block"> | |
<div class="card-block__label"></div> | |
<div class="input-group"> | |
<input class="input none" type="text" /> | |
</div> | |
<!-- <div class="dummy-photo"></div> --> | |
<div class="card-block__info"> | |
<span> | |
<i class="icon-photos"></i> | |
<span class="info-text">x 0</span> | |
</span> | |
<div class="dropdown"> | |
<a href="#" class="link" data-toggle="dropdown"> | |
<i class="icon-dots-white"></i> | |
</a> | |
<div class="card-dropdown-menu dropdown-menu dropdown-menu-right"> | |
<a class="dropdown-item edit-label-menu" href="#" data-locale="edit_label"></a> | |
<a class="dropdown-item clear-menu" href="#" data-locale="clear"></a> | |
</div> | |
</div> | |
</div> | |
<button class="button is-w100 is-mini"> | |
<i class="icon-camera is-mini"></i> | |
</button> | |
</div> | |
`; | |
$('#learning .card-block-container').append(html); | |
} | |
updateCounts() { | |
const counts = this.knnClassifier.getCountByLabel(); | |
for(let i=0;i<NUM_CLASSES; i++){ | |
this.infoTexts[i].innerText = `x ${counts[String(i)] || 0}` | |
} | |
} | |
} | |
window.addEventListener('load', () => { | |
new Main(); | |
new I18n(); | |
}); | |