Commit 764b3a75 authored by Sugon_ldc's avatar Sugon_ldc
Browse files

add new model

parents
plugins {
id 'com.android.application'
}
repositories {
jcenter()
maven {
url "https://oss.sonatype.org/content/repositories/snapshots"
}
}
android {
signingConfigs {
release {
storeFile file('wenet.keystore')
storePassword '123456'
keyAlias 'wenet'
keyPassword '123456'
}
}
packagingOptions {
pickFirst 'lib/arm64-v8a/libc++_shared.so'
}
configurations {
extractForNativeBuild
}
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.mobvoi.wenet"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
targets "wenet", "decoder_main"
cppFlags "-std=c++14", "-DC10_USE_GLOG", "-DC10_USE_MINIMAL_GLOG", "-DANDROID", "-Wno-c++11-narrowing", "-fexceptions"
}
}
ndkVersion '21.1.6352462'
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'org.pytorch:pytorch_android:1.10.0'
extractForNativeBuild 'org.pytorch:pytorch_android:1.10.0'
implementation 'com.github.pengzhendong:wenet-openfst-android:1.0.2'
extractForNativeBuild 'com.github.pengzhendong:wenet-openfst-android:1.0.2'
}
task extractAARForNativeBuild {
doLast {
configurations.extractForNativeBuild.files.each {
def file = it.absoluteFile
copy {
from zipTree(file)
into "$buildDir/$file.name"
include "headers/**"
include "jni/**"
}
}
}
}
tasks.whenTaskAdded { task ->
if (task.name.contains('externalNativeBuild')) {
task.dependsOn(extractAARForNativeBuild)
}
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package com.mobvoi.wenet;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.mobvoi.wenet", appContext.getPackageName());
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.mobvoi.wenet">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
tools:replace="android:theme"
android:theme="@style/Theme.Wenet">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
put final.zip and units.txt here.
cmake_minimum_required(VERSION 3.4.1)
set(TARGET wenet)
project(${TARGET} CXX)
set(CMAKE_CXX_STANDARD 14)
include(ExternalProject)
option(TORCH "whether to build with Torch" ON)
option(ONNX "whether to build with ONNX" OFF)
set(CMAKE_VERBOSE_MAKEFILE on)
set(build_DIR ${CMAKE_SOURCE_DIR}/../../../build)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
string(REPLACE "-Wl,--exclude-libs,libgcc_real.a" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
include(libtorch)
include(openfst)
include_directories(
${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/kaldi
)
add_subdirectory(utils)
add_subdirectory(frontend)
add_subdirectory(post_processor)
add_subdirectory(kaldi) # kaldi: wfst based decoder
add_subdirectory(decoder)
link_libraries(frontend decoder android)
add_library(${TARGET} SHARED wenet.cc)
add_executable(decoder_main bin/decoder_main.cc)
target_link_libraries(decoder_main PUBLIC libc++_shared.so)
../../../../../core/bin
\ No newline at end of file
../../../../../core/cmake
\ No newline at end of file
../../../../../core/decoder
\ No newline at end of file
../../../../../core/frontend
\ No newline at end of file
../../../../../core/kaldi
\ No newline at end of file
../../../../../core/patch
\ No newline at end of file
../../../../../core/post_processor
\ No newline at end of file
../../../../../core/utils
\ No newline at end of file
// Copyright (c) 2021 Mobvoi Inc (authors: Xiaoyu Chen)
//
// 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
//
// http://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.
#include <jni.h>
#include "torch/script.h"
#include "torch/torch.h"
#include "decoder/asr_decoder.h"
#include "decoder/torch_asr_model.h"
#include "frontend/feature_pipeline.h"
#include "frontend/wav.h"
#include "post_processor/post_processor.h"
#include "utils/log.h"
#include "utils/string.h"
namespace wenet {
std::shared_ptr<DecodeOptions> decode_config;
std::shared_ptr<FeaturePipelineConfig> feature_config;
std::shared_ptr<FeaturePipeline> feature_pipeline;
std::shared_ptr<AsrDecoder> decoder;
std::shared_ptr<DecodeResource> resource;
DecodeState state = kEndBatch;
std::string total_result; // NOLINT
void init(JNIEnv* env, jobject, jstring jModelDir) {
const char* pModelDir = env->GetStringUTFChars(jModelDir, nullptr);
std::string modelPath = std::string(pModelDir) + "/final.zip";
std::string dictPath = std::string(pModelDir) + "/units.txt";
auto model = std::make_shared<TorchAsrModel>();
model->Read(modelPath);
LOG(INFO) << "model path: " << modelPath;
resource = std::make_shared<DecodeResource>();
resource->model = model;
resource->symbol_table = std::shared_ptr<fst::SymbolTable>(
fst::SymbolTable::ReadText(dictPath));
LOG(INFO) << "dict path: " << dictPath;
PostProcessOptions post_process_opts;
resource->post_processor =
std::make_shared<PostProcessor>(post_process_opts);
feature_config = std::make_shared<FeaturePipelineConfig>(80, 16000);
feature_pipeline = std::make_shared<FeaturePipeline>(*feature_config);
decode_config = std::make_shared<DecodeOptions>();
decode_config->chunk_size = 16;
decoder = std::make_shared<AsrDecoder>(feature_pipeline, resource,
*decode_config);
}
void reset(JNIEnv *env, jobject) {
LOG(INFO) << "wenet reset";
decoder->Reset();
state = kEndBatch;
total_result = "";
}
void accept_waveform(JNIEnv *env, jobject, jshortArray jWaveform) {
jsize size = env->GetArrayLength(jWaveform);
int16_t* waveform = env->GetShortArrayElements(jWaveform, 0);
feature_pipeline->AcceptWaveform(waveform, size);
LOG(INFO) << "wenet accept waveform in ms: " << int(size / 16);
}
void set_input_finished() {
LOG(INFO) << "wenet input finished";
feature_pipeline->set_input_finished();
}
void decode_thread_func() {
while (true) {
state = decoder->Decode();
if (state == kEndFeats || state == kEndpoint) {
decoder->Rescoring();
}
std::string result;
if (decoder->DecodedSomething()) {
result = decoder->result()[0].sentence;
}
if (state == kEndFeats) {
LOG(INFO) << "wenet endfeats final result: " << result;
total_result += result;
break;
} else if (state == kEndpoint) {
LOG(INFO) << "wenet endpoint final result: " << result;
total_result += result + ",";
decoder->ResetContinuousDecoding();
} else {
if (decoder->DecodedSomething()) {
LOG(INFO) << "wenet partial result: " << result;
}
}
}
}
void start_decode() {
std::thread decode_thread(decode_thread_func);
decode_thread.detach();
}
jboolean get_finished(JNIEnv *env, jobject) {
if (state == kEndFeats) {
LOG(INFO) << "wenet recognize finished";
return JNI_TRUE;
}
return JNI_FALSE;
}
jstring get_result(JNIEnv *env, jobject) {
std::string result;
if (decoder->DecodedSomething()) {
result = decoder->result()[0].sentence;
}
LOG(INFO) << "wenet ui result: " << total_result + result;
return env->NewStringUTF((total_result + result).c_str());
}
} // namespace wenet
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass c = env->FindClass("com/mobvoi/wenet/Recognize");
if (c == nullptr) {
return JNI_ERR;
}
static const JNINativeMethod methods[] = {
{"init", "(Ljava/lang/String;)V", reinterpret_cast<void*>(wenet::init)},
{"reset", "()V", reinterpret_cast<void *>(wenet::reset)},
{"acceptWaveform", "([S)V",
reinterpret_cast<void *>(wenet::accept_waveform)},
{"setInputFinished", "()V",
reinterpret_cast<void *>(wenet::set_input_finished)},
{"getFinished", "()Z", reinterpret_cast<void *>(wenet::get_finished)},
{"startDecode", "()V", reinterpret_cast<void *>(wenet::start_decode)},
{"getResult", "()Ljava/lang/String;",
reinterpret_cast<void *>(wenet::get_result)},
};
int rc = env->RegisterNatives(c, methods,
sizeof(methods) / sizeof(JNINativeMethod));
if (rc != JNI_OK) {
return rc;
}
return JNI_VERSION_1_6;
}
package com.mobvoi.wenet;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Process;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class MainActivity extends AppCompatActivity {
private final int MY_PERMISSIONS_RECORD_AUDIO = 1;
private static final String LOG_TAG = "WENET";
private static final int SAMPLE_RATE = 16000; // The sampling rate
private static final int MAX_QUEUE_SIZE = 2500; // 100 seconds audio, 1 / 0.04 * 100
private static final List<String> resource = Arrays.asList(
"final.zip", "units.txt", "ctc.ort", "decoder.ort", "encoder.ort"
);
private boolean startRecord = false;
private AudioRecord record = null;
private int miniBufferSize = 0; // 1280 bytes 648 byte 40ms, 0.04s
private final BlockingQueue<short[]> bufferQueue = new ArrayBlockingQueue<>(MAX_QUEUE_SIZE);
public static void assetsInit(Context context) throws IOException {
AssetManager assetMgr = context.getAssets();
// Unzip all files in resource from assets to context.
// Note: Uninstall the APP will remove the resource files in the context.
for (String file : assetMgr.list("")) {
if (resource.contains(file)) {
File dst = new File(context.getFilesDir(), file);
if (!dst.exists() || dst.length() == 0) {
Log.i(LOG_TAG, "Unzipping " + file + " to " + dst.getAbsolutePath());
InputStream is = assetMgr.open(file);
OutputStream os = new FileOutputStream(dst);
byte[] buffer = new byte[4 * 1024];
int read;
while ((read = is.read(buffer)) != -1) {
os.write(buffer, 0, read);
}
os.flush();
}
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
if (requestCode == MY_PERMISSIONS_RECORD_AUDIO) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i(LOG_TAG, "record permission is granted");
initRecorder();
} else {
Toast.makeText(this, "Permissions denied to record audio", Toast.LENGTH_LONG).show();
Button button = findViewById(R.id.button);
button.setEnabled(false);
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
requestAudioPermissions();
try {
assetsInit(this);
} catch (IOException e) {
Log.e(LOG_TAG, "Error process asset files to file path");
}
TextView textView = findViewById(R.id.textView);
textView.setText("");
Recognize.init(getFilesDir().getPath());
Button button = findViewById(R.id.button);
button.setText("Start Record");
button.setOnClickListener(view -> {
if (!startRecord) {
startRecord = true;
Recognize.reset();
startRecordThread();
startAsrThread();
Recognize.startDecode();
button.setText("Stop Record");
} else {
startRecord = false;
Recognize.setInputFinished();
button.setText("Start Record");
}
button.setEnabled(false);
});
}
private void requestAudioPermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.RECORD_AUDIO},
MY_PERMISSIONS_RECORD_AUDIO);
} else {
initRecorder();
}
}
private void initRecorder() {
// buffer size in bytes 1280
miniBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
if (miniBufferSize == AudioRecord.ERROR || miniBufferSize == AudioRecord.ERROR_BAD_VALUE) {
Log.e(LOG_TAG, "Audio buffer can't initialize!");
return;
}
record = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
miniBufferSize);
if (record.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(LOG_TAG, "Audio Record can't initialize!");
return;
}
Log.i(LOG_TAG, "Record init okay");
}
private void startRecordThread() {
new Thread(() -> {
VoiceRectView voiceView = findViewById(R.id.voiceRectView);
record.startRecording();
Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO);
while (startRecord) {
short[] buffer = new short[miniBufferSize / 2];
int read = record.read(buffer, 0, buffer.length);
voiceView.add(calculateDb(buffer));
try {
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
bufferQueue.put(buffer);
}
} catch (InterruptedException e) {
Log.e(LOG_TAG, e.getMessage());
}
Button button = findViewById(R.id.button);
if (!button.isEnabled() && startRecord) {
runOnUiThread(() -> button.setEnabled(true));
}
}
record.stop();
voiceView.zero();
}).start();
}
private double calculateDb(short[] buffer) {
double energy = 0.0;
for (short value : buffer) {
energy += value * value;
}
energy /= buffer.length;
energy = (10 * Math.log10(1 + energy)) / 100;
energy = Math.min(energy, 1.0);
return energy;
}
private void startAsrThread() {
new Thread(() -> {
// Send all data
while (startRecord || bufferQueue.size() > 0) {
try {
short[] data = bufferQueue.take();
// 1. add data to C++ interface
Recognize.acceptWaveform(data);
// 2. get partial result
runOnUiThread(() -> {
TextView textView = findViewById(R.id.textView);
textView.setText(Recognize.getResult());
});
} catch (InterruptedException e) {
Log.e(LOG_TAG, e.getMessage());
}
}
// Wait for final result
while (true) {
// get result
if (!Recognize.getFinished()) {
runOnUiThread(() -> {
TextView textView = findViewById(R.id.textView);
textView.setText(Recognize.getResult());
});
} else {
runOnUiThread(() -> {
Button button = findViewById(R.id.button);
button.setEnabled(true);
});
break;
}
}
}).start();
}
}
\ No newline at end of file
package com.mobvoi.wenet;
public class Recognize {
static {
System.loadLibrary("wenet");
}
public static native void init(String modelDir);
public static native void reset();
public static native void acceptWaveform(short[] waveform);
public static native void setInputFinished();
public static native boolean getFinished();
public static native void startDecode();
public static native String getResult();
}
package com.mobvoi.wenet;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
import androidx.core.content.ContextCompat;
import java.util.Arrays;
/**
* 自定义的音频模拟条形图 Created by shize on 2016/9/5.
*/
public class VoiceRectView extends View {
// 音频矩形的数量
private int mRectCount;
// 音频矩形的画笔
private Paint mRectPaint;
// 渐变颜色的两种
private int topColor, downColor;
// 音频矩形的宽和高
private int mRectWidth, mRectHeight;
// 偏移量
private int offset;
// 频率速度
private int mSpeed;
private double[] mEnergyBuffer = null;
public VoiceRectView(Context context) {
this(context, null);
}
public VoiceRectView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VoiceRectView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setPaint(context, attrs);
}
public void setPaint(Context context, AttributeSet attrs) {
// 将属性存储到TypedArray中
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.VoiceRect);
mRectPaint = new Paint();
// 添加矩形画笔的基础颜色
mRectPaint.setColor(ta.getColor(R.styleable.VoiceRect_RectTopColor,
ContextCompat.getColor(context, R.color.top_color)));
// 添加矩形渐变色的上面部分
topColor = ta.getColor(R.styleable.VoiceRect_RectTopColor,
ContextCompat.getColor(context, R.color.top_color));
// 添加矩形渐变色的下面部分
downColor = ta.getColor(R.styleable.VoiceRect_RectDownColor,
ContextCompat.getColor(context, R.color.down_color));
// 设置矩形的数量
mRectCount = ta.getInt(R.styleable.VoiceRect_RectCount, 10);
mEnergyBuffer = new double[mRectCount];
// 设置重绘的时间间隔,也就是变化速度
mSpeed = ta.getInt(R.styleable.VoiceRect_RectSpeed, 300);
// 每个矩形的间隔
offset = ta.getInt(R.styleable.VoiceRect_RectOffset, 0);
// 回收TypeArray
ta.recycle();
}
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
super.onSizeChanged(w, h, oldW, oldH);
// 渐变效果
LinearGradient mLinearGradient;
// 画布的宽
int mWidth;
// 获取画布的宽
mWidth = getWidth();
// 获取矩形的最大高度
mRectHeight = getHeight();
// 获取单个矩形的宽度(减去的部分为到右边界的间距)
mRectWidth = (mWidth - offset) / mRectCount;
// 实例化一个线性渐变
mLinearGradient = new LinearGradient(
0,
0,
mRectWidth,
mRectHeight,
topColor,
downColor,
Shader.TileMode.CLAMP
);
// 添加进画笔的着色器
mRectPaint.setShader(mLinearGradient);
}
public void add(double energy) {
if (mEnergyBuffer.length - 1 >= 0) {
System.arraycopy(mEnergyBuffer, 1, mEnergyBuffer, 0, mEnergyBuffer.length - 1);
}
mEnergyBuffer[mEnergyBuffer.length - 1] = energy;
}
public void zero() {
Arrays.fill(mEnergyBuffer, 0);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
double mRandom;
float currentHeight;
for (int i = 0; i < mRectCount; i++) {
// 由于只是简单的案例就不监听音频输入,随机模拟一些数字即可
mRandom = Math.random();
//if (i < 1 || i > mRectCount - 2) mRandom = 0;
currentHeight = (float) (mRectHeight * mEnergyBuffer[i]);
// 矩形的绘制是从左边开始到上、右、下边(左右边距离左边画布边界的距离,上下边距离上边画布边界的距离)
canvas.drawRect(
(float) (mRectWidth * i + offset),
(mRectHeight - currentHeight) / 2,
(float) (mRectWidth * (i + 1)),
mRectHeight / 2 + currentHeight / 2,
mRectPaint
);
}
// 使得view延迟重绘
postInvalidateDelayed(mSpeed);
}
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment