[update] tfjs-node の aarch64 未対応により、ピュアJS(WASM)版へ移行。それに伴って face-api から脱却(ヤレヤレ)

This commit is contained in:
ocogeclub 2022-02-14 14:59:34 +09:00
parent bbb18cbd14
commit bbf0077d6e
10 changed files with 1160 additions and 4569 deletions

View File

@ -665,16 +665,55 @@
<field name="sound">meow</field>
</block>
<label text="顔認識" web-line="4.0" web-line-width="200"></label>
<block type="ugj_faceapi">
<field name="with_landmark">FALSE</field>
</block>
<block type="ugj_face_init"></block>
<block type="ugj_face_display"></block>
<block type="ugj_face_detect"></block>
<block type="ugj_face_coord">
<field name="face_coord">0</field>
<block type="ugj_face_detect">
<value name="preditions">
<block type="variables_get">
<field name="VAR" id="TLZRpW`yXuYE3Z31)B=2">検出結果</field>
</block>
</value>
</block>
<block type="ugj_face_location">
<field name="member">topLeft[0]</field>
<value name="prediction">
<block type="lists_getIndex">
<mutation statement="false" at="true"></mutation>
<field name="MODE">GET</field>
<field name="WHERE">FROM_START</field>
<value name="VALUE">
<block type="variables_get">
<field name="VAR" id="_W]y2e!_~suF]yM;LQ1~">検出結果</field>
</block>
</value>
<value name="AT">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
</value>
</block>
<block type="ugj_face_draw">
<field name="with_landmark">TRUE</field>
<value name="prediction">
<block type="lists_getIndex">
<mutation statement="false" at="true"></mutation>
<field name="MODE">GET</field>
<field name="WHERE">FROM_START</field>
<value name="VALUE">
<block type="variables_get">
<field name="VAR" id="E-Bsl~,RZG]E)v`k![_p">検出結果</field>
</block>
</value>
<value name="AT">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
</value>
</block>
<block type="ugj_face_drawrect"></block>
<label text="_" web-line="4.0" web-line-width="200"></label>
</category>

View File

@ -389,9 +389,12 @@ const ugj_createCode = (args) => {
// await使用のため、必要に応じてコード全体をasync付き即時関数でラップ
if (addAsync) {
code = [
'(async () => {',
// '(async () => {',
'const main = async () => {',
code,
'})();'
`}`,
`main();`,
// '})();'
].join('\n');
}
// コードを綺麗に

View File

@ -1,39 +0,0 @@
[
{
"weights":
[
{"name":"dense0/conv0/filters","shape":[3,3,3,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008194216092427571,"min":-0.9423348506291708}},
{"name":"dense0/conv0/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006839508168837603,"min":-0.8412595047670252}},
{"name":"dense0/conv1/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009194007106855804,"min":-1.2779669878529567}},
{"name":"dense0/conv1/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0036026100317637128,"min":-0.3170296827952067}},
{"name":"dense0/conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.000740380117706224,"min":-0.06367269012273527}},
{"name":"dense0/conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":1,"min":0}},
{"name":"dense0/conv2/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":1,"min":0}},
{"name":"dense0/conv2/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0037702228508743585,"min":-0.6220867703942692}},
{"name":"dense1/conv0/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0033707996209462483,"min":-0.421349952618281}},
{"name":"dense1/conv0/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.014611541991140328,"min":-1.8556658328748217}},
{"name":"dense1/conv0/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002832523046755323,"min":-0.30307996600281956}},
{"name":"dense1/conv1/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006593170586754294,"min":-0.6329443763284123}},
{"name":"dense1/conv1/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.012215249211180444,"min":-1.6001976466646382}},
{"name":"dense1/conv1/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002384825547536214,"min":-0.3028728445370992}},
{"name":"dense1/conv2/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005859645441466687,"min":-0.7617539073906693}},
{"name":"dense1/conv2/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013121426806730382,"min":-1.7845140457153321}},
{"name":"dense1/conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0032247188044529336,"min":-0.46435950784122243}},
{"name":"dense2/conv0/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002659512618008782,"min":-0.32977956463308894}},
{"name":"dense2/conv0/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015499923743453681,"min":-1.9839902391620712}},
{"name":"dense2/conv0/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0032450980999890497,"min":-0.522460794098237}},
{"name":"dense2/conv1/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005911862382701799,"min":-0.792189559282041}},
{"name":"dense2/conv1/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021025861478319356,"min":-2.2077154552235325}},
{"name":"dense2/conv1/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00349616945958605,"min":-0.46149436866535865}},
{"name":"dense2/conv2/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008104994250278847,"min":-1.013124281284856}},
{"name":"dense2/conv2/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.029337059282789044,"min":-3.5791212325002633}},
{"name":"dense2/conv2/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0038808938334969913,"min":-0.4230174278511721}},
{"name":"fc/weights","shape":[128,136],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.014016061670639936,"min":-1.8921683255363912}},
{"name":"fc/bias","shape":[136],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0029505149698724935,"min":0.088760145008564}}
],
"paths":
[
"face_landmark_68_tiny_model.bin"
]
}
]

View File

@ -1,30 +0,0 @@
[
{
"weights":
[
{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},
{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},
{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},
{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},
{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},
{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},
{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},
{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},
{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},
{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},
{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},
{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},
{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},
{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},
{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},
{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},
{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},
{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},
{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}
],
"paths":
[
"tiny_face_detector_model.bin"
]
}
]

View File

@ -1,8 +0,0 @@
var count=10;
var id = setInterval(() => {
console.log(count--);
if (count<0) {
console.log(' : 終了 - Node Test JS');
clearInterval(id);
}
}, 1000);

5409
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "ocoge",
"version": "0.1.4",
"version": "0.1.5",
"description": "ブロックベースビジュアルプログラム開発・実行環境",
"main": "main.js",
"scripts": {
@ -35,12 +35,11 @@
"@ocogeclub/bme280": "file:local_modules/@ocogeclub/bme280",
"@ocogeclub/paj7620": "file:local_modules/@ocogeclub/paj7620",
"@ocogeclub/pigpio": "file:local_modules/@ocogeclub/pigpio",
"@tensorflow-models/blazeface": "^0.0.7",
"@tensorflow-models/knn-classifier": "^1.2.2",
"@tensorflow-models/mobilenet": "^2.1.0",
"@tensorflow/tfjs-converter": "^3.13.0",
"@tensorflow/tfjs-core": "^3.13.0",
"@tensorflow/tfjs-node": "^3.13.0",
"@vladmandic/face-api": "^1.6.5",
"@tensorflow/tfjs": "^3.13.0",
"@tensorflow/tfjs-backend-wasm": "^3.13.0",
"axios": "^0.25.0",
"electron-squirrel-startup": "^1.0.0",
"nodemailer": "^6.7.2",
@ -77,4 +76,4 @@
]
}
}
}
}

View File

@ -1288,53 +1288,35 @@ Blockly.Python['ugj_grideye_stop'] = function (block) {
/******************** */
/** Face Detection ** */
/******************** */
var with_landmark;
Blockly.Blocks['ugj_faceapi'] = {
init: function () {
this.appendDummyInput()
.appendField("TensorFlowによる顔検出を秒に")
.appendField(new Blockly.FieldDropdown([["1", "1000"], ["2", "500"], ["5", "200"], ["10", "100"]]), "interval_sec")
.appendField("回実行:")
.appendField(new Blockly.FieldCheckbox("TRUE"), "with_landmark")
.appendField("ランドマークを検出");
this.appendStatementInput("do")
.setCheck(null);
this.setTooltip("TensorFlow とFaceAPI をロードし、顔検出をできるようにします。");
this.setHelpUrl("");
this.setStyle('multimedia_blocks');
}
};
Blockly.JavaScript['ugj_faceapi'] = function (block) {
var dropdown_interval_sec = block.getFieldValue('interval_sec');
with_landmark = block.getFieldValue('with_landmark') == 'TRUE';
var statements_do = Blockly.JavaScript.statementToCode(block, 'do');
var code = [
`require('@tensorflow/tfjs-node');`,
`const _faceapi = require('@vladmandic/face-api/dist/face-api.node.js');`,
`const _interval_sec = ${dropdown_interval_sec};`,
statements_do,
''
].join('\n');
return code;
};
Blockly.Blocks['ugj_face_init'] = {
init: function () {
this.appendDummyInput()
.appendField("顔検出のビデオを開始");
.appendField("顔検出を開始");
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setStyle('multimedia_blocks');
this.setTooltip("顔検出のためのビデオストリームを開始します。");
this.setTooltip("Blazeface detector モデルによる顔検出を開始します。最初に実行してください");
this.setHelpUrl("");
}
};
Blockly.JavaScript['ugj_face_init'] = function (block) {
Blockly.JavaScript.provideFunction_(
'require_tfjs', [`const _tf = require('@tensorflow/tfjs');`]
);
Blockly.JavaScript.provideFunction_(
'require_tfjs_wasm', [`require('@tensorflow/tfjs-backend-wasm');`]
);
Blockly.JavaScript.provideFunction_(
'require_blazeface', [`const _blazeface = require('@tensorflow-models/blazeface');`]
);
var code = [
`await _tf.setBackend('wasm');`,
"const _videoEl = document.getElementById('subdisplay');",
`const _displaySize = { width: _videoEl.width, height: _videoEl.height };`,
"const _stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: _displaySize });",
"_videoEl.srcObject = _stream;",
`const _model = await _blazeface.load();`,
""
].join('\n');
return code;
@ -1342,11 +1324,11 @@ Blockly.JavaScript['ugj_face_init'] = function (block) {
Blockly.Blocks['ugj_face_display'] = {
init: function () {
this.appendDummyInput()
.appendField("ビデオを表示");
.appendField("顔検出ビデオを表示");
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setStyle('multimedia_blocks')
this.setStyle('multimedia_blocks');
this.setTooltip("カメラの映像を画像エリアに表示します。必須ではないブロックです。");
this.setHelpUrl("");
}
@ -1359,108 +1341,91 @@ Blockly.JavaScript['ugj_face_display'] = function (block) {
`_overlay.setAttribute('height', _videoEl.height);`,
`_overlay.className = 'subdisplay';`,
`document.getElementById('display_area').appendChild(_overlay);`,
`const _overlay_ctx = _overlay.getContext('2d');`,
""
].join('\n');
return code;
};
Blockly.Blocks['ugj_face_detect'] = {
init: function () {
this.appendValueInput("preditions")
.setCheck("Variable")
.appendField("顔検出を実行し、結果をリスト");
this.appendDummyInput()
.appendField("顔を検出したら");
this.appendStatementInput("do")
.setCheck(null);
.appendField("に代入する");
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setTooltip("顔を発見したら動作します。位置データは「顔の座標」ブロックで参照します。");
this.setNextStatement(true, null);
this.setTooltip("顔検出を実行します。検出結果はリストになります。顔の位置は「顔の座標」ブロックで参照します。");
this.setHelpUrl("");
this.setStyle('multimedia_blocks');
}
};
Blockly.JavaScript['ugj_face_detect'] = function (block) {
var statements_do = Blockly.JavaScript.statementToCode(block, 'do');
var code_model = `await _faceapi.nets.tinyFaceDetector.load('${elutil.path.join(elutil.library_path, 'models/')}');`;
if (with_landmark) { code_model += `\nawait _faceapi.nets.faceLandmark68TinyNet.load('${elutil.path.join(elutil.library_path, 'models/')}');`; }
var code_detect_face = " let _result = await _faceapi.detectSingleFace(_videoEl, _options)"
if (with_landmark) { code_detect_face += `.withFaceLandmarks(true);` }
else { code_detect_face += `;` }
var code_rect;
if (with_landmark) {
code_rect = [
` _coord[0] = Math.round(_result.detection.box.x);`,
` _coord[1] = Math.round(_result.detection.box.y);`,
` _coord[2] = Math.round(_result.detection.box.width);`,
` _coord[3] = Math.round(_result.detection.box.height);`,
].join('\n');
} else {
code_rect = [
` _coord[0] = Math.round(_result.box.x);`,
` _coord[1] = Math.round(_result.box.y);`,
` _coord[2] = Math.round(_result.box.width);`,
` _coord[3] = Math.round(_result.box.height);`,
].join('\n');
}
var code = [
code_model,
"const _options = new _faceapi.TinyFaceDetectorOptions({ inputSize: 128, scoreThreshold : 0.3 });",
// `const _onPlay = async () => {`,
` const _detectInterval = setInterval(async () => {`,
code_detect_face,
" if (_result) {",
` let _coord = [];`,
code_rect,
statements_do,
" }",
` }, _interval_sec);`,
// " setTimeout(() => onPlay())",
// "}",
// "_videoEl.onplay = _onPlay;",
""
].join('\n');
var value_preditions = Blockly.JavaScript.valueToCode(block, 'preditions', Blockly.JavaScript.ORDER_ATOMIC);
var code = `${value_preditions} = await _model.estimateFaces(_videoEl, false);`;
return code;
};
Blockly.Blocks['ugj_face_coord'] = {
Blockly.Blocks['ugj_face_location'] = {
init: function () {
this.appendValueInput("prediction")
.setCheck("Array")
.appendField("顔");
this.appendDummyInput()
.appendField("顔の座標")
.appendField(new Blockly.FieldDropdown([["左", "0"], ["上", "1"], ["幅", "2"], ["高さ", "3"]]), "face_coord");
.appendField("の")
.appendField(new Blockly.FieldDropdown([["左座標", "topLeft[0]"], ["上座標", "topLeft[1]"], ["右座標", "bottomRight[0]"], ["下座標", "bottomRight[1]"]]), "member");
this.setInputsInline(true);
this.setOutput(true, null);
this.setTooltip("顔の座標(位置)を参照します。");
this.setOutput(true, "Number");
this.setTooltip("顔検出結果の座標を参照します。");
this.setHelpUrl("");
this.setStyle('multimedia_blocks');
}
};
Blockly.JavaScript['ugj_face_coord'] = function (block) {
var dropdown_face_coord = block.getFieldValue('face_coord');
var code = `_coord[${dropdown_face_coord}]`;
Blockly.JavaScript['ugj_face_location'] = function (block) {
var value_prediction = Blockly.JavaScript.valueToCode(block, 'prediction', Blockly.JavaScript.ORDER_NONE);
var dropdown_member = block.getFieldValue('member');
var code = `${value_prediction}.${dropdown_member}`;
return [code, Blockly.JavaScript.ORDER_NONE];
};
//
Blockly.Blocks['ugj_face_drawrect'] = {
Blockly.Blocks['ugj_face_draw'] = {
init: function () {
this.appendValueInput("prediction")
.setCheck("Variable")
.appendField("顔");
this.appendDummyInput()
.appendField("検出結果を描画");
.appendField("を描画:")
.appendField(new Blockly.FieldCheckbox("TRUE"), "with_landmark")
.appendField("ランドマーク");
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setStyle('multimedia_blocks');
this.setTooltip("ビデオに検出結果を四角や点で表示します。「ビデオを表示」ブロックが必要です。");
this.setTooltip("顔検出結果をビデオ画面に描画します。「ビデオを表示」ブロックが必要です。");
this.setHelpUrl("");
this.setStyle('multimedia_blocks');
}
};
Blockly.JavaScript['ugj_face_drawrect'] = function (block) {
var code_draw = ` _faceapi.draw.drawDetections(_overlay, _resizedDetections);`;
if (with_landmark) { code_draw += `\n _faceapi.draw.drawFaceLandmarks(_overlay, _resizedDetections);`; }
var code = [
` _faceapi.matchDimensions(_overlay, _displaySize);`,
` const _resizedDetections = _faceapi.resizeResults(_result, _displaySize);`,
code_draw,
""
].join('\n');
Blockly.JavaScript['ugj_face_draw'] = function (block) {
var value_prediction = Blockly.JavaScript.valueToCode(block, 'prediction', Blockly.JavaScript.ORDER_NONE);
var checkbox_with_landmark = block.getFieldValue('with_landmark') === 'TRUE';
var code = `const _start = ${value_prediction}.topLeft;
const _end = ${value_prediction}.bottomRight;
const _size = [_end[0] - _start[0], _end[1] - _start[1]];
_overlay_ctx.clearRect(0, 0, _displaySize.width, _displaySize.height)
_overlay_ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
_overlay_ctx.fillRect(_start[0], _start[1], _size[0], _size[1]);
if (${checkbox_with_landmark}) {
const _landmarks = ${value_prediction}.landmarks;
_overlay_ctx.fillStyle = 'blue';
for (let _j = 0; _j < _landmarks.length; _j++) {
const _x = _landmarks[_j][0];
const _y = _landmarks[_j][1];
_overlay_ctx.fillRect(_x, _y, 5, 5);
}
}
`;
return code;
};
/**************************** */
/** Say while some seconds ** */
/**************************** */
@ -1883,7 +1848,11 @@ Blockly.Blocks['ugj_teachable_machine'] = {
};
Blockly.JavaScript['ugj_teachable_machine'] = function (block) {
Blockly.JavaScript.provideFunction_(
'require_ts', [`const _tf = require('@tensorflow/tfjs-node');`]
'require_ts', [`const _tf = require('@tensorflow/tfjs');`]
// 'require_ts', [`const _tf = require('@tensorflow/tfjs-node');`]
);
Blockly.JavaScript.provideFunction_(
'require_wasm', [`const _wasm = require('@tensorflow/tfjs-backend-wasm');`]
);
Blockly.JavaScript.provideFunction_(
'require_mobilenet', [`const _mobilenet = require('@tensorflow-models/mobilenet');`]
@ -1892,8 +1861,9 @@ Blockly.JavaScript['ugj_teachable_machine'] = function (block) {
'require_knn', [`const _knnClassifier = require('@tensorflow-models/knn-classifier');`]
);
var code = `const _classifier = _knnClassifier.create();
var code = `await _tf.setBackend('wasm');
const _net = await _mobilenet.load({ version: 1, alpha: 0.25 }); // 高速・低精度
const _classifier = _knnClassifier.create();
`;
return code;
};