33 |
34 | #if !defined(AIUI_MBEDTLS_PLATFORM_ZEROIZE_ALT)
35 | /*
36 | * This implementation should never be optimized out by the compiler
37 | *
38 | * This implementation for mbedtls_platform_zeroize() was inspired from Colin
39 | * Percival's blog article at:
40 | *
41 | * http://www.daemonology.net/blog/2014-09-04-how-to-zero-a-buffer.html
42 | *
43 | * It uses a volatile function pointer to the standard memset(). Because the
44 | * pointer is volatile the compiler expects it to change at
45 | * any time and will not optimize out the call that could potentially perform
46 | * other operations on the input buffer instead of just setting it to 0.
47 | * Nevertheless, as pointed out by davidtgoldblatt on Hacker News
48 | * (refer to http://www.daemonology.net/blog/2014-09-05-erratum.html for
49 | * details), optimizations of the following form are still possible:
50 | *
51 | * if( memset_func != memset )
52 | * memset_func( buf, 0, len );
53 | *
54 | * Note that it is extremely difficult to guarantee that
55 | * mbedtls_platform_zeroize() will not be optimized out by aggressive compilers
56 | * in a portable way. For this reason, Mbed TLS also provides the configuration
57 | * option AIUI_MBEDTLS_PLATFORM_ZEROIZE_ALT, which allows users to configure
58 | * mbedtls_platform_zeroize() to use a suitable implementation for their
59 | * platform and needs.
60 | */
61 | static void * (* const volatile memset_func)( void *, int, size_t ) = memset;
62 |
63 | void mbedtls_platform_zeroize( void *buf, size_t len )
64 | {
65 | memset_func( buf, 0, len );
66 | }
67 | #endif /* AIUI_MBEDTLS_PLATFORM_ZEROIZE_ALT */
68 |
--------------------------------------------------------------------------------
/AIUIChatDemo/app/src/main/java/com/iflytek/aiui/demo/chat/ui/common/SingleLiveEvent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 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 | package com.iflytek.aiui.demo.chat.ui.common;
18 |
19 | import android.arch.lifecycle.LifecycleOwner;
20 | import android.arch.lifecycle.MutableLiveData;
21 | import android.arch.lifecycle.Observer;
22 | import android.support.annotation.MainThread;
23 | import android.support.annotation.Nullable;
24 | import android.util.Log;
25 |
26 | import java.util.concurrent.atomic.AtomicBoolean;
27 |
28 | /**
29 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like
30 | * navigation and Snackbar messages.
31 | *
32 | * This avoids a common problem with events: on configuration change (like rotation) an update
33 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an
34 | * explicit call to setValue() or call().
35 | *
36 | * Note that only one observer is going to be notified of changes.
37 | */
38 | public class SingleLiveEvent extends MutableLiveData {
39 |
40 | private static final String TAG = "SingleLiveEvent";
41 |
42 | private final AtomicBoolean mPending = new AtomicBoolean(false);
43 |
44 | @MainThread
45 | public void observe(LifecycleOwner owner, final Observer observer) {
46 |
47 | if (hasActiveObservers()) {
48 | Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
49 | }
50 |
51 | // Observe the internal MutableLiveData
52 | super.observe(owner, new Observer() {
53 | @Override
54 | public void onChanged(@Nullable T t) {
55 | if (mPending.compareAndSet(true, false)) {
56 | observer.onChanged(t);
57 | }
58 | }
59 | });
60 | }
61 |
62 | @MainThread
63 | public void setValue(@Nullable T t) {
64 | mPending.set(true);
65 | super.setValue(t);
66 | }
67 |
68 | /**
69 | * Used for cases where T is Void, to make calls cleaner.
70 | */
71 | @MainThread
72 | public void call() {
73 | setValue(null);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/AIUIChatDemo/app/src/main/res/values/attr.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/AIUIChatDemo/app/src/main/java/com/iflytek/aiui/demo/chat/model/handler/DefaultHandler.java:
--------------------------------------------------------------------------------
1 | package com.iflytek.aiui.demo.chat.model.handler;
2 |
3 | import android.text.TextUtils;
4 |
5 | import com.iflytek.aiui.demo.chat.model.data.SemanticResult;
6 | import com.iflytek.aiui.demo.chat.repository.player.AIUIPlayer;
7 | import com.iflytek.aiui.demo.chat.ui.PermissionChecker;
8 | import com.iflytek.aiui.demo.chat.ui.chat.ChatViewModel;
9 | import com.iflytek.aiui.demo.chat.ui.chat.PlayerViewModel;
10 |
11 | import org.json.JSONArray;
12 | import org.json.JSONObject;
13 |
14 | import java.util.ArrayList;
15 | import java.util.List;
16 | import java.util.regex.Pattern;
17 |
18 | /**
19 | * Created by PR on 2017/8/4.
20 | */
21 |
22 | public class DefaultHandler extends Handler {
23 | private Pattern typePattern = Pattern.compile("\\d");
24 |
25 | public DefaultHandler(ChatViewModel model, PlayerViewModel player, PermissionChecker checker, SemanticResult result) {
26 | super(model, player, checker, result);
27 | }
28 |
29 | @Override
30 | public String getFormatContent() {
31 | if(result.data != null) {
32 | List songList = new ArrayList<>();
33 | JSONArray list = result.data.optJSONArray("result");
34 | if(list != null){
35 | for(int index = 0; index < list.length(); index++){
36 | JSONObject item = list.optJSONObject(index);
37 | int contentType = item.optInt("type", -1);
38 | switch (contentType) {
39 | //文本内容
40 | case 0:{
41 | //显示第一条结果
42 | result.answer += NEWLINE_NO_HTML + NEWLINE_NO_HTML + item.optString("content");
43 | break;
44 | }
45 | //音频内容(1) 视频内容(2)
46 | case 1:
47 | case 2: {
48 | String audioPath = item.optString("url");
49 | String songname = item.optString("title");
50 | if(TextUtils.isEmpty(songname)) {
51 | songname = item.optString("name");
52 | }
53 |
54 | songList.add(new AIUIPlayer.SongInfo(songname, songname, audioPath));
55 | break;
56 | }
57 | }
58 | }
59 | }
60 |
61 | if(songList.size() != 0) {
62 | mPlayer.playList(songList);
63 | if(isNeedShowContrlTip()) {
64 | result.answer = result.answer + NEWLINE_NO_HTML + NEWLINE_NO_HTML + CONTROL_TIP;
65 | }
66 | }
67 | }
68 |
69 | return result.answer;
70 |
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/aiui/c-sharp/aiui_csharp_demo/IAIUIAgent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace aiui
5 | {
6 | class IAIUIAgent
7 | {
8 | public delegate void AIUIMessageCallback(IAIUIEvent ev);
9 |
10 | private IntPtr mAgent = IntPtr.Zero;
11 |
12 | private AIUIMessageCallback messageCallback = null;
13 | private AIUIMessageCallback_ onEvent_ = null;
14 |
15 | private void OnEvent(IntPtr ev_, IntPtr data)
16 | {
17 | IAIUIEvent ev = new IAIUIEvent(ev_);
18 | messageCallback?.Invoke(ev);
19 | ev = null;
20 | }
21 |
22 | private IAIUIAgent(string param, AIUIMessageCallback cb)
23 | {
24 | if (IntPtr.Zero == mAgent)
25 | {
26 | messageCallback = cb;
27 | onEvent_ = new AIUIMessageCallback_(OnEvent);
28 | mAgent = aiui_agent_create(Marshal.StringToHGlobalAnsi(param), onEvent_, IntPtr.Zero);
29 | }
30 | }
31 |
32 | public static IAIUIAgent Create(string param, AIUIMessageCallback cb)
33 | {
34 | return new IAIUIAgent(param, cb);
35 | }
36 |
37 | public void SendMessage(IAIUIMessage msg)
38 | {
39 | if (IntPtr.Zero != mAgent)
40 | aiui_agent_send_message(mAgent, msg.Ptr);
41 | }
42 |
43 | ~IAIUIAgent()
44 | {
45 | Destroy();
46 | }
47 |
48 | public void Destroy()
49 | {
50 | if (IntPtr.Zero != mAgent)
51 | {
52 | aiui_agent_destroy(mAgent);
53 | mAgent = IntPtr.Zero;
54 | }
55 |
56 | messageCallback = null;
57 | onEvent_ = null;
58 | }
59 |
60 | public static string Version()
61 | {
62 | IntPtr temp = aiui_get_version();
63 | string res = Marshal.PtrToStringAnsi(temp).ToString();
64 | temp = IntPtr.Zero;
65 |
66 | return res;
67 | }
68 |
69 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
70 | private delegate void AIUIMessageCallback_(IntPtr ev, IntPtr data);
71 |
72 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
73 | private extern static IntPtr aiui_agent_create(IntPtr param, AIUIMessageCallback_ cb, IntPtr data);
74 |
75 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
76 | private extern static void aiui_agent_send_message(IntPtr agent, IntPtr msg);
77 |
78 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
79 | private extern static void aiui_agent_destroy(IntPtr agent);
80 |
81 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
82 | private extern static IntPtr aiui_get_version();
83 | }
84 | }
--------------------------------------------------------------------------------
/AIUIChatDemo/app/src/main/java/com/iflytek/aiui/demo/chat/model/handler/HintHandler.java:
--------------------------------------------------------------------------------
1 | package com.iflytek.aiui.demo.chat.model.handler;
2 |
3 | import android.text.TextUtils;
4 |
5 | import com.iflytek.aiui.demo.chat.model.data.SemanticResult;
6 | import com.iflytek.aiui.demo.chat.ui.PermissionChecker;
7 | import com.iflytek.aiui.demo.chat.ui.chat.ChatViewModel;
8 | import com.iflytek.aiui.demo.chat.ui.chat.PlayerViewModel;
9 |
10 | /**
11 | * Created by PR on 2017/12/19.
12 | */
13 |
14 | public class HintHandler extends Handler {
15 |
16 | private final StringBuilder defaultAnswer;
17 |
18 | public HintHandler(ChatViewModel model, PlayerViewModel player, PermissionChecker checker, SemanticResult result) {
19 | super(model, player, checker, result);
20 | defaultAnswer = new StringBuilder();
21 | defaultAnswer.append("你好,我不懂你的意思");
22 | defaultAnswer.append(Handler.NEWLINE_NO_HTML);
23 | defaultAnswer.append(Handler.NEWLINE_NO_HTML);
24 | defaultAnswer.append("在后台添加更多技能让我变得更强大吧 :D");
25 | }
26 |
27 |
28 | // defaultAnswer.append(Handler.NEWLINE);
29 | // defaultAnswer.append(Handler.NEWLINE);
30 | // defaultAnswer.append("你也可以了解我的更多特性");
31 |
32 | @Override
33 | public String getFormatContent() {
34 | if(TextUtils.isEmpty(result.answer)) {
35 | return defaultAnswer.toString();
36 | } else {
37 | return result.answer;
38 | }
39 | }
40 |
41 | @Override
42 | public boolean urlClicked(String url) {
43 | if ("more".equals(url)) {
44 | StringBuilder more = new StringBuilder();
45 | more.append("1. 用户个性化");
46 | mMessageViewModel.fakeAIUIResult(0, "unknown", more.toString());
47 | } else if ("individual".equals(url)) {
48 | StringBuilder more = new StringBuilder();
49 | more.append("1. 电话");
50 | more.append(NEWLINE);
51 | more.append("通过上传本地的联系人信息,可以让电话技能更准确地理解您的意思");
52 | more.append(NEWLINE);
53 | more.append(NEWLINE);
54 | more.append("2. 点菜");
55 | more.append(NEWLINE);
56 | more.append("通过点菜的技能,根据用户选择的不同分店的菜单,提供精准的识别和语义");
57 | more.append(NEWLINE);
58 | more.append(NEWLINE);
59 | more.append("3. 菜谱");
60 | more.append(NEWLINE);
61 | more.append("通过菜谱技能,根据信源结果预测下次用户交互的热词,提高识别和语义的精准度");
62 | mMessageViewModel.fakeAIUIResult(0, "unknown", more.toString());
63 | } else if ("telephone".equals(url)) {
64 | mMessageViewModel.sendText("打电话给妈妈");
65 | } else if ("menu".equals(url)) {
66 | mMessageViewModel.sendText("我要点菜");
67 | } else if ("dish".equals(url)) {
68 | mMessageViewModel.sendText("有哪些川菜");
69 | }
70 | return super.urlClicked(url);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/AIUIChatDemo/app/src/main/java/com/iflytek/aiui/demo/chat/model/handler/PlayerHandler.java:
--------------------------------------------------------------------------------
1 | package com.iflytek.aiui.demo.chat.model.handler;
2 |
3 | import com.iflytek.aiui.demo.chat.model.data.SemanticResult;
4 | import com.iflytek.aiui.demo.chat.repository.player.AIUIPlayer;
5 | import com.iflytek.aiui.demo.chat.ui.PermissionChecker;
6 | import com.iflytek.aiui.demo.chat.ui.chat.ChatViewModel;
7 | import com.iflytek.aiui.demo.chat.ui.chat.PlayerViewModel;
8 |
9 | import org.json.JSONArray;
10 | import org.json.JSONObject;
11 |
12 | import java.util.ArrayList;
13 | import java.util.List;
14 |
15 | /**
16 | * Created by PR on 2017/12/21.
17 | */
18 |
19 | public class PlayerHandler extends Handler {
20 |
21 |
22 | public PlayerHandler(ChatViewModel model, PlayerViewModel player, PermissionChecker checker, SemanticResult result) {
23 | super(model, player, checker, result);
24 | }
25 |
26 | @Override
27 | public String getFormatContent() {
28 | String intent = result.semantic.optString("intent");
29 | if(intent.equals("INSTRUCTION")) {
30 | JSONArray slots = result.semantic.optJSONArray("slots");
31 | for(int index=0;index < slots.length(); index++) {
32 | JSONObject slot = slots.optJSONObject(index);
33 | if("insType".equals(slot.optString("name"))){
34 | String instruction = slot.optString("value");
35 | if("next".equals(instruction)) {
36 | mPlayer.next();
37 | } else if("past".equals(instruction)) {
38 | mPlayer.prev();
39 | } else if("pause".equals(instruction)) {
40 | mPlayer.pause();
41 | } else if("replay".equals(instruction)) {
42 | mPlayer.play();
43 | }
44 | }
45 | }
46 |
47 | return "已完成操作";
48 | } else {
49 | if(result.data != null) {
50 | List songList = new ArrayList<>();
51 | JSONArray list = result.data.optJSONArray("result");
52 | if(list != null){
53 | for(int index = 0; index < list.length(); index++){
54 | JSONObject audio = list.optJSONObject(index);
55 | String audioPath = audio.optString("audiopath");
56 | String songname = audio.optString("songname");
57 | String author = audio.optJSONArray("singernames").optString(0);
58 |
59 | songList.add(new AIUIPlayer.SongInfo(author, songname, audioPath));
60 | }
61 |
62 | }
63 |
64 | if(songList.size() != 0) {
65 | mPlayer.playList(songList);
66 | if(isNeedShowContrlTip()) {
67 | result.answer = result.answer + NEWLINE_NO_HTML + NEWLINE_NO_HTML + CONTROL_TIP;
68 | }
69 | }
70 | }
71 | return result.answer;
72 | }
73 |
74 | }
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/AIUIChatDemo/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | ext {
3 | support_ver = '26.1.0'
4 | }
5 | android {
6 | compileSdkVersion 26
7 | buildToolsVersion '26.0.2'
8 | defaultConfig {
9 | applicationId "com.iflytek.aiui.demo.chat"
10 | minSdkVersion 18
11 | targetSdkVersion 25
12 | versionCode 19
13 | versionName "1.3.7"
14 | vectorDrawables.useSupportLibrary = true
15 | ndk {
16 | abiFilters 'armeabi'
17 | }
18 | }
19 |
20 | dataBinding {
21 | enabled = true
22 | }
23 |
24 | lintOptions {
25 | abortOnError false
26 | }
27 |
28 | buildTypes {
29 | release {
30 | minifyEnabled false
31 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
32 | }
33 | }
34 |
35 | flavorDimensions 'version'
36 | productFlavors {
37 | sample {
38 | dimension 'version'
39 | versionName "${defaultConfig.versionName}.sample"
40 |
41 | if(file('src/main/jniLibs/armeabi/libmsc.so').exists()) {
42 | buildConfigField 'Boolean', 'WAKEUP_ENABLE', 'true'
43 | } else {
44 | buildConfigField 'Boolean', 'WAKEUP_ENABLE', 'false'
45 | }
46 | }
47 | }
48 |
49 | applicationVariants.all { variant ->
50 | variant.outputs.all {
51 | outputFileName = "AIUIChatDemo-${variant.flavorName}-${variant.buildType.name}-${variant.versionName}.apk"
52 | }
53 | }
54 |
55 | dexOptions {
56 | jumboMode true
57 | }
58 | }
59 |
60 | dependencies {
61 | implementation fileTree(dir: 'libs', include: ['*.jar'])
62 |
63 | implementation 'com.google.dagger:dagger:2.12'
64 | implementation 'com.google.android.gms:play-services-plus:11.8.0'
65 | annotationProcessor 'com.google.dagger:dagger-compiler:2.12'
66 | annotationProcessor 'com.google.dagger:dagger-android-processor:2.12'
67 | implementation 'com.google.dagger:dagger-android:2.12'
68 | implementation 'com.google.dagger:dagger-android-support:2.12'
69 | implementation "com.android.support:support-vector-drawable:$support_ver"
70 | implementation "com.android.support:design:$support_ver"
71 | implementation "com.android.support:appcompat-v7:$support_ver"
72 | implementation "com.android.support:cardview-v7:$support_ver"
73 | implementation "com.android.support:preference-v7:$support_ver"
74 | implementation "com.android.support:preference-v14:$support_ver"
75 | implementation 'com.android.support.constraint:constraint-layout:1.0.2'
76 | testImplementation 'junit:junit:4.12'
77 |
78 | implementation "android.arch.lifecycle:extensions:1.1.0"
79 | annotationProcessor "android.arch.lifecycle:compiler:1.1.0"
80 | implementation "android.arch.persistence.room:runtime:1.0.0"
81 | annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
82 |
83 | implementation "io.reactivex.rxjava2:rxjava:2.1.2"
84 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
85 | implementation 'com.zzhoujay.richtext:richtext:2.5.4'
86 | implementation 'com.karumi:dexter:4.1.0'
87 | implementation 'com.daasuu:BubbleLayout:1.2.0'
88 | implementation 'com.github.Jay-Goo:WaveLineView:v1.0.3'
89 | implementation 'com.pddstudio:highlightjs-android:1.5.0'
90 | implementation 'com.google.android.exoplayer:exoplayer-core:2.6.1'
91 | implementation 'com.jakewharton.timber:timber:4.6.1'
92 | }
93 |
94 |
95 |
--------------------------------------------------------------------------------
/AIUIChatDemo/app/src/main/java/com/iflytek/aiui/demo/chat/ui/chat/MessageListAdapter.java:
--------------------------------------------------------------------------------
1 | package com.iflytek.aiui.demo.chat.ui.chat;
2 |
3 | import android.databinding.DataBindingUtil;
4 | import android.text.Spannable;
5 | import android.view.LayoutInflater;
6 | import android.view.MotionEvent;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 |
10 | import com.iflytek.aiui.demo.chat.R;
11 | import com.iflytek.aiui.demo.chat.databinding.ChatItemBinding;
12 | import com.iflytek.aiui.demo.chat.model.InteractMessage;
13 | import com.zzhoujay.richtext.RichText;
14 | import com.zzhoujay.richtext.ext.LongClickableLinkMovementMethod;
15 |
16 | import java.util.Arrays;
17 |
18 | /**
19 | * Created by PR on 2017/7/31.
20 | */
21 |
22 | public class MessageListAdapter extends DataBoundListAdapter
23 | implements ItemListener {
24 | private ChatFragment mFragment;
25 | public MessageListAdapter(ChatFragment fragment){
26 | mFragment = fragment;
27 | }
28 | @Override
29 | protected ChatItemBinding createBinding(ViewGroup parent, int viewType) {
30 | return DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),
31 | R.layout.chat_item, parent, false);
32 |
33 | }
34 |
35 | @Override
36 | protected void bind(final ChatItemBinding binding, InteractMessage item) {
37 | binding.setMsg(item);
38 | binding.setBinding(binding);
39 | binding.setHandler(this);
40 | String content = item.getDisplayText();
41 | if(content.contains("")){
42 | RichText.from(content)
43 | .urlClick(item.getHandler())
44 | .into(binding.chatItemContentText);
45 | binding.chatItemContentText.setOnTouchListener(new View.OnTouchListener() {
46 | @Override
47 | public boolean onTouch(View view, MotionEvent motionEvent) {
48 | return new LongClickableLinkMovementMethod()
49 | .onTouchEvent(binding.chatItemContentText, (Spannable) binding.chatItemContentText.getText(), motionEvent);
50 | }
51 | });
52 | } else {
53 | binding.chatItemContentText.setText(item.getDisplayText());
54 | }
55 |
56 | }
57 |
58 | @Override
59 | protected boolean areItemsTheSame(InteractMessage oldItem, InteractMessage newItem) {
60 | return oldItem.getMessage().timestamp == newItem.getMessage().timestamp;
61 | }
62 |
63 | @Override
64 | protected boolean areContentsTheSame(InteractMessage oldItem, InteractMessage newItem) {
65 | if(Arrays.equals(oldItem.getMessage().msgData, newItem.getMessage().msgData)) {
66 | String oldContent = oldItem.getMessage().cacheContent;
67 | String newContent = newItem.getMessage().cacheContent;
68 | return oldContent == null? newContent == null : oldContent.equals(newContent);
69 | } else {
70 | return false;
71 | }
72 |
73 | }
74 |
75 | @Override
76 | public void onMessageClick(InteractMessage msg, ChatItemBinding binding) {
77 | // if(msg.getMessage().msgType == Message.MsgType.Voice) {
78 | // AudioHandler.getsInstance().playAudioMessage(msg.getMessage());
79 | // }
80 |
81 | if(!msg.getMessage().isFromUser()) {
82 | mFragment.switchToDetail(new String(msg.getMessage().msgData));
83 | }
84 |
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/webapi_v2_entity/doc/动态实体webapi.md:
--------------------------------------------------------------------------------
1 | # API说明
2 |
3 | * 授权认证,调用接口需要将nameSpace,nonce,curtime和checkSum信息放在HTTP请求头中。
4 | * 所有接口统一为UTF-8编码。
5 | * 所有接口支持http和https。
6 |
7 | # 授权认证
8 |
9 | 在调用所有业务接口时,都需要在Http Request Header中加入以下参数作为授权验证
10 |
11 | |参数名|说明|是否必须|
12 | | ------|----------------------|--------|
13 | | X-NameSpace | aiui开放平台个人中心的命名空间 |是|
14 | | X-Nonce |随机数(最大长度128个字符)|是|
15 | | X-CurTime |当前UTC时间戳,从1970年1月1日0点0 分0 秒开始到现在的秒数(String)|是|
16 | | X-CheckSum |MD5(accountKey + Nonce + CurTime),三个参数拼接的字符串,进行MD5哈希计算|是|
17 |
18 | 注:
19 |
20 | * CheckSum有效期:出于安全性考虑,每个CheckSum的有效期为5分钟(用curTime计算),同时CurTime要与标准时间同步,否则,时间相差太大,服务端会直接认为CurTime无效。
21 |
22 | * checkSum生成示例,例如:
23 |
24 | accountKey是abcd1234, Nonce是12, CurTime是1502607694。那么CheckSum为MD5(abcd1234121502607694)
25 | 最终MD5为32位小写 bf5aa1f53bd173cf7413bf370ad4bddc
26 |
27 | # IP 白名单
28 |
29 | IP 白名单具备打开和关闭两种状态。
30 |
31 | * 当 IP 白名单打开时,用户在调用所有业务接口时,在授权认证通过后,检查调用方ip是否在aiui开放平台配置的ip白名单中。若在,则向用户提供服务,否则拒绝提供服务。
32 |
33 | * 当 IP 白名单关闭时,任意终端均可访问 AIUI 服务器,开发者需要自行保证 nameSpace 和 key 值安全。
34 |
35 | 注:拒绝提供服务返回值:{"code":"20004","desc":"ip非法","data":null,"sid":"rwabb52e660@dx6c9b0e56f81d3ef000"}
36 |
37 | # 通用请求地址
38 |
39 | base_url:openapi.xfyun.cn
40 |
41 | # AIUI接口
42 |
43 | 通用返回参数
44 |
45 | |参数名|说明|是否必须|
46 | | ------|----------------------|--------|
47 | |code |结果码 |是|
48 | |data |返回结果 |是|
49 | |desc |描述 |是|
50 | |sid |本次webapi服务唯一标识 |是|
51 |
52 | ## 动态实体
53 | ### 上传资源
54 |
55 | * 接口描述
56 |
57 | 本接口提供动态实体上传资源功能,用于动态更新实体资源。
58 |
59 | * 接口地址
60 |
61 | POST /v2/aiui/entity/upload-resource HTTP/1.1
62 |
63 | Content-Type:application/x-www-form-urlencoded; charset=utf-8
64 |
65 | * 参数说明
66 |
67 | |参数|类型|必须|说明|示例
|
68 | | ------|-------|-------|--------|--------|
69 | |appid|string|是|应用id|5adde3cf|
70 | |res_name|String|是|资源名,XXX为用户的命名空间|XXX.music|
71 | |pers_param|String|是|个性化参数(json)|{"appid":"xxxxxx"}|
72 | |data|String|是|Base64编码的资源|示例1|
73 |
74 | 其中,pers_param为个性化参数。示例如下:
75 |
76 | |维度|示例|说明|
77 | | ------|-------|-------|
78 | |应用级|{"appid":"xxxxxx"}||
79 | |用户级|{"auth_id":"d3b6d50a9f8194b623b5e2d4e298c9d6"}|auth_id为用户唯一ID(32位字符串,包括英文小写字母与数字,开发者需保证该值与终端用户一一对应)|
80 | |自定义级|{"xxxxxx":"xxxxxx"}||
81 |
82 | data为web页面定义的主子段、从字段给的json格式对应的base64。例如,主子段为song、从字段singer,上传资源的格式为:
83 |
84 | {"song":"给我一首歌的时间","singer":"周杰伦"}
85 | {"song":"忘情水","singer":"刘德华"}
86 | {"song":"暗香","singer":"刘德华"}
87 | {"song":"逆光","singer":"梁静茹"}
88 |
89 | 注:每条数据之间用换行符隔开。
90 |
91 | Base64编码为
92 |
93 | 示例1:
94 |
95 | eyJzb25nIjoi57uZ5oiR5LiA6aaW5q2M55qE5pe26Ze0Iiwic2luZ2VyIjoi5ZGo5p2w5LymIn0NCnsic29uZyI6IuW/mOaDheawtCIsInNpbmdlciI6IuWImOW+t+WNjiJ9DQp7InNvbmciOiLmmpfpppkiLCJzaW5nZXIiOiLliJjlvrfljY4ifQ0KeyJzb25nIjoi6YCG5YWJIiwic2luZ2VyIjoi5qKB6Z2Z6Iy5In0=
96 |
97 | * 返回说明
98 |
99 | |参数 |类型 |必须 |说明 |示例|
100 | | ------|-------|-------|--------|--------|
101 | |sid |String |是 |本次上传sid,可用于查看上传资源是否成功|psn003478f3@ch00070e3a78e06f2601|
102 | |csid |String |是 |本次服务唯一标识|rwa84b7a73b@ch372d0e3a78e0116200|
103 |
104 | ### 查看上传资源是否成功
105 |
106 | * 接口描述
107 |
108 | 本接口提供检查动态实体上传资源是否成功。
109 |
110 | 注:上传资源数据后至少间隔3秒后再进行查看上传资源是否成功
111 |
112 | * 接口地址
113 |
114 | POST /v2/aiui/entity/check-resource HTTP/1.1
115 |
116 | Content-Type:application/x-www-form-urlencoded; charset=utf-8
117 |
118 | * 参数说明
119 |
120 | |参数|类型|必须|说明|示例|
121 | | ------|-------|-------|--------|--------|
122 | |sid|string|是|sid|psn开头的sid|
123 |
124 | * 返回说明
125 |
126 | |参数 |类型 |必须 |说明 |
127 | | ------|-------|-------|--------|
128 | |sid |String |是 |上传sid|
129 | |csid |String |是 |上传sid|
130 | |reply |String |是 |查看上传资源是否成功描述|
131 | |error |int |是 |查看上传资源是否成功错误码|
--------------------------------------------------------------------------------
/websocket/go/aiui_websocket_demo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "golang.org/x/net/websocket"
6 | "io/ioutil"
7 | "strconv"
8 | "time"
9 | "net/http"
10 | "encoding/base64"
11 | "crypto/md5"
12 | )
13 |
14 | // 数据结束发送标记
15 | const BREAK_FLAG = "--end--"
16 | // 每帧数据大小,单位:字节
17 | const SLICE_SIZE = 1280
18 | // AIUI websocket服务地址
19 | const WS_URL = "ws://wsapi.xfyun.cn/v1/aiui"
20 | const ORIGIN = "http://wsapi.xfyun.cn"
21 | // 应用ID,在AIUI开放平台创建并设置
22 | const APPID = ""
23 | // 接口密钥,在AIUI开放平台查看
24 | const API_KEY = ""
25 | // 发送的数据文件位置
26 | const FILE_PATH = "date.pcm"
27 |
28 | // 调用方式:运行 main(),控制台输出云端返回结果
29 | func main() {
30 | curtime := strconv.Itoa(int(time.Now().Unix()))
31 | param := `{"auth_id":"2894c985bf8b1111c6728db79d3479ae","data_type":"audio","aue":"raw","scene":"main","sample_rate":"16000"}`
32 | paramBase64 := base64.URLEncoding.EncodeToString([]byte(param))
33 | checksum := Md5Encode(API_KEY + curtime + paramBase64)
34 | // websocket 握手
35 | url := WS_URL + "?appid=" + APPID + "&checksum=" + checksum + "&curtime=" + curtime + "¶m=" + paramBase64
36 | config, _ := websocket.NewConfig(url, ORIGIN)
37 | config.Protocol = []string{"13"}
38 | header := make(map[string][] string)
39 | header["X-Real-Ip"] = []string{"114.116.69.134"}
40 | config.Header = http.Header(header)
41 | conn, err := websocket.DialConfig(config)
42 | if err != nil {
43 | fmt.Errorf("websocket dial err: %v", err)
44 | return
45 | }
46 | defer conn.Close()
47 |
48 | sendChan := make(chan int, 1)
49 | receiveChan := make(chan int, 1)
50 | defer close(sendChan)
51 | defer close(receiveChan)
52 | // 发送数据
53 | go send(conn, sendChan)
54 | // 接收数据
55 | go receive(conn, receiveChan)
56 | <-sendChan
57 | <-receiveChan
58 | return
59 | }
60 |
61 | // 发送数据
62 | func send(conn *websocket.Conn, sendChan chan int) {
63 | // 分帧发送音频数据
64 | audio1, _ := ioutil.ReadFile(FILE_PATH)
65 | if err := sendBySlice(conn, audio1); err != nil {
66 | fmt.Errorf("send data err: %v", err)
67 | sendChan <- 1
68 | return
69 | }
70 | // 发送结束符
71 | if err := websocket.Message.Send(conn, BREAK_FLAG); err != nil {
72 | fmt.Errorf("send break flag err: %v", err)
73 | sendChan <- 1
74 | return
75 | }
76 |
77 | sendChan <- 1
78 | return
79 | }
80 |
81 | // 分片发送数据
82 | func sendBySlice(conn *websocket.Conn, data []byte) (err error) {
83 | sliceNum := getSliceNum(len(data), SLICE_SIZE)
84 | for i := 0; i < sliceNum; i++ {
85 | var sliceData []byte
86 | if (i+1)*SLICE_SIZE < len(data) {
87 | sliceData = data[i*SLICE_SIZE : (i+1)*SLICE_SIZE]
88 | } else {
89 | sliceData = data[i*SLICE_SIZE:]
90 | }
91 | if err = websocket.Message.Send(conn, sliceData); err != nil {
92 | fmt.Errorf("send msg err: %v", err)
93 | return err
94 | }
95 | time.Sleep(time.Duration(40 * time.Millisecond))
96 | }
97 | return nil
98 | }
99 |
100 | // 接收结果
101 | func receive(conn *websocket.Conn, readChan chan int) {
102 | for {
103 | var msg string
104 | if err := websocket.Message.Receive(conn, &msg); err != nil {
105 | if err.Error() == "EOF" {
106 | fmt.Println("receive msg end")
107 | }else{
108 | fmt.Errorf("receive msg error: %v", msg)
109 | }
110 | readChan <- 1
111 | return
112 | }
113 | fmt.Println("receive msg: %v", msg)
114 | }
115 | readChan <- 1
116 | }
117 |
118 | // 计算数据帧数
119 | func getSliceNum(dataSize, sliceSize int) int {
120 | if dataSize%sliceSize == 0 {
121 | return dataSize / sliceSize
122 | } else {
123 | return dataSize/sliceSize + 1
124 | }
125 | }
126 |
127 | // 计算字符串MD5值
128 | func Md5Encode(str string) (strMd5 string) {
129 | strByte := []byte(str)
130 | strMd5Byte := md5.Sum(strByte)
131 | strMd5 = fmt.Sprintf("%x", strMd5Byte)
132 | return strMd5
133 | }
--------------------------------------------------------------------------------
/AIUIChatDemo/app/src/main/java/com/iflytek/aiui/demo/chat/ui/common/widget/PopupWindowFactory.java:
--------------------------------------------------------------------------------
1 | package com.iflytek.aiui.demo.chat.ui.common.widget;
2 |
3 | import android.content.Context;
4 | import android.view.KeyEvent;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 | import android.widget.PopupWindow;
8 |
9 |
10 | /**
11 | * 作者:Rance on 2016/11/29 10:47
12 | * 邮箱:rance935@163.com
13 | */
14 | public class PopupWindowFactory {
15 |
16 | private Context mContext;
17 |
18 | private PopupWindow mPop;
19 |
20 | /**
21 | * @param mContext 上下文
22 | * @param view PopupWindow显示的布局文件
23 | */
24 | public PopupWindowFactory(Context mContext, View view){
25 | this(mContext,view, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
26 | }
27 |
28 | /**
29 | * @param mContext 上下文
30 | * @param view PopupWindow显示的布局文件
31 | * @param width PopupWindow的宽
32 | * @param heigth PopupWindow的高
33 | */
34 | public PopupWindowFactory(Context mContext, View view, int width, int heigth){
35 | init(mContext,view,width,heigth);
36 | }
37 |
38 |
39 | private void init(Context mContext, View view, int width, int heigth){
40 | this.mContext = mContext;
41 |
42 | //下面这两个必须有!!
43 | view.setFocusable(true);
44 | view.setFocusableInTouchMode(true);
45 |
46 | // PopupWindow(布局,宽度,高度)
47 | mPop = new PopupWindow(view,width,heigth,true);
48 | mPop.setOutsideTouchable(false);
49 | mPop.setFocusable(true);
50 |
51 | // 重写onKeyListener,按返回键消失
52 | view.setOnKeyListener(new View.OnKeyListener() {
53 | @Override
54 | public boolean onKey(View v, int keyCode, KeyEvent event) {
55 | if (keyCode == KeyEvent.KEYCODE_BACK) {
56 | mPop.dismiss();
57 | return true;
58 | }
59 | return false;
60 | }
61 | });
62 |
63 | //点击其他地方消失
64 | // view.setOnTouchListener(new View.OnTouchListener() {
65 | // @Override
66 | // public boolean onTouch(View v, MotionEvent event) {
67 | // if (mPop != null && mPop.isShowing()) {
68 | // mPop.dismiss();
69 | // return true;
70 | // }
71 | // return false;
72 | // }});
73 |
74 |
75 | }
76 |
77 | public PopupWindow getPopupWindow(){
78 | return mPop;
79 | }
80 |
81 |
82 | /**
83 | * 以触发弹出窗的view为基准,出现在view的内部上面,弹出的pop_view左上角正对view的左上角
84 | * @param parent view
85 | * @param gravity 在view的什么位置 Gravity.CENTER、Gravity.TOP......
86 | * @param x 与控件的x坐标距离
87 | * @param y 与控件的y坐标距离
88 | */
89 | public void showAtLocation(View parent, int gravity, int x, int y){
90 |
91 | if(mPop.isShowing()){
92 | return ;
93 | }
94 | mPop.showAtLocation(parent, gravity, x, y);
95 |
96 | }
97 |
98 | /**
99 | * 以触发弹出窗的view为基准,出现在view的正下方,弹出的pop_view左上角正对view的左下角
100 | * @param anchor view
101 | */
102 | public void showAsDropDown(View anchor){
103 | showAsDropDown(anchor,0,0);
104 | }
105 |
106 | /**
107 | * 以触发弹出窗的view为基准,出现在view的正下方,弹出的pop_view左上角正对view的左下角
108 | * @param anchor view
109 | * @param xoff 与view的x坐标距离
110 | * @param yoff 与view的y坐标距离
111 | */
112 | public void showAsDropDown(View anchor, int xoff, int yoff){
113 | if(mPop.isShowing()){
114 | return ;
115 | }
116 |
117 | mPop.showAsDropDown(anchor, xoff, yoff);
118 | }
119 |
120 | /**
121 | * 隐藏PopupWindow
122 | */
123 | public void dismiss(){
124 | if (mPop.isShowing()) {
125 | mPop.dismiss();
126 | }
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/AIUIChatDemo/app/src/main/java/com/iflytek/aiui/demo/chat/ui/settings/SettingsFragment.java:
--------------------------------------------------------------------------------
1 | package com.iflytek.aiui.demo.chat.ui.settings;
2 |
3 | import android.arch.lifecycle.Observer;
4 | import android.arch.lifecycle.ViewModelProvider;
5 | import android.arch.lifecycle.ViewModelProviders;
6 | import android.content.Context;
7 | import android.content.SharedPreferences;
8 | import android.os.Bundle;
9 | import android.support.annotation.Nullable;
10 | import android.support.design.widget.Snackbar;
11 | import android.support.v7.preference.EditTextPreference;
12 | import android.support.v7.preference.PreferenceFragmentCompat;
13 | import android.support.v7.preference.SwitchPreferenceCompat;
14 |
15 | import com.iflytek.aiui.demo.chat.R;
16 |
17 | import javax.inject.Inject;
18 |
19 | import dagger.android.support.AndroidSupportInjection;
20 |
21 | /**
22 | * Created by PR on 2017/12/12.
23 | */
24 |
25 | public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
26 | public static final String AIUI_EOS = "aiui_eos";
27 | public static final String AIUI_WAKEUP = "aiui_wakeup";
28 | @Inject
29 | ViewModelProvider.Factory mViewModelFactory;
30 | private SettingViewModel mSettingModel;
31 | private EditTextPreference eosPreference;
32 | private SwitchPreferenceCompat wakeupPreference;
33 |
34 | @Override
35 | public void onAttach(Context context) {
36 | super.onAttach(context);
37 | AndroidSupportInjection.inject(this);
38 | }
39 |
40 | @Override
41 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
42 | addPreferencesFromResource(R.xml.pref_settings);
43 | eosPreference = (EditTextPreference) (getPreferenceManager().findPreference(AIUI_EOS));
44 | eosPreference.setSummary(String.format("%sms", getPreferenceManager().getSharedPreferences().getString(AIUI_EOS, "1000")));
45 | wakeupPreference = (SwitchPreferenceCompat) getPreferenceManager().findPreference(AIUI_WAKEUP);
46 | }
47 |
48 | @Override
49 | public void onActivityCreated(Bundle savedInstanceState) {
50 | super.onActivityCreated(savedInstanceState);
51 | mSettingModel = ViewModelProviders.of(this, mViewModelFactory).get(SettingViewModel.class);
52 |
53 | mSettingModel.isWakeUpAvailable().observe(this, new Observer() {
54 | @Override
55 | public void onChanged(@Nullable Boolean enable) {
56 | wakeupPreference.setEnabled(enable);
57 | }
58 | });
59 | }
60 |
61 | @Override
62 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
63 | if(AIUI_EOS.equals(s)){
64 | String eos = sharedPreferences.getString(s, "1000");
65 | if(!isNumeric(eos)) {
66 | eosPreference.setText("1000");
67 | Snackbar.make(getView(), R.string.eos_invalid_tip , Snackbar.LENGTH_LONG).show();
68 | } else {
69 | eosPreference.setSummary(String.format("%sms", eos));
70 | }
71 | }
72 | }
73 |
74 |
75 | private boolean isNumeric(String str) {
76 | try {
77 | Integer.valueOf(str);
78 | return true;
79 | } catch (Exception e) {
80 | return false;
81 | }
82 | }
83 |
84 |
85 |
86 | @Override
87 | public void onResume() {
88 | super.onResume();
89 | getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
90 | }
91 |
92 | @Override
93 | public void onPause() {
94 | super.onPause();
95 | getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
96 | }
97 |
98 | @Override
99 | public void onStop() {
100 | super.onStop();
101 |
102 | mSettingModel.syncLastSetting();
103 | }
104 |
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/aiui/c-sharp/aiui_csharp_demo/AIUISetting.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace aiui
5 | {
6 | class AIUISetting
7 | {
8 | public enum LogLevel { _debug, _info, _warn, _error, _none };
9 |
10 | public static bool SetAIUIDir(string dir)
11 | {
12 | return aiui_set_aiui_dir(Marshal.StringToHGlobalAnsi(dir));
13 | }
14 |
15 | public static bool SetMscDir(string dir)
16 | {
17 | return aiui_set_msc_dir(Marshal.StringToHGlobalAnsi(dir));
18 | }
19 |
20 | public static bool SetMscCfg(string cfg)
21 | {
22 | return aiui_set_msc_cfg(Marshal.StringToHGlobalAnsi(cfg));
23 | }
24 |
25 | public static bool InitLogger(string dir)
26 | {
27 | return aiui_init_logger(Marshal.StringToHGlobalAnsi(dir));
28 | }
29 |
30 | public static void SetLogLevel(LogLevel level)
31 | {
32 | aiui_set_log_level(level);
33 | }
34 |
35 | public static void SetNetLogLevel(LogLevel level)
36 | {
37 | aiui_set_net_log_level(level);
38 | }
39 |
40 | public static void setSaveDataLog(bool save, int size)
41 | {
42 | aiui_set_save_data_log(save, size);
43 | }
44 |
45 | public static bool setDataLogDir(string dir)
46 | {
47 | return aiui_set_data_log_dir(Marshal.StringToHGlobalAnsi(dir));
48 | }
49 |
50 | public static void setSystemInfo(string key, string val)
51 | {
52 | aiui_set_system_info(Marshal.StringToHGlobalAnsi(key), Marshal.StringToHGlobalAnsi(val));
53 | }
54 |
55 | public static bool setRawAudioDir(string dir)
56 | {
57 | return aiui_set_raw_audio_dir(Marshal.StringToHGlobalAnsi(dir));
58 | }
59 |
60 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
61 | private extern static bool aiui_set_aiui_dir(IntPtr szDir);
62 |
63 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
64 | private extern static IntPtr aiui_get_aiui_dir();
65 |
66 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
67 | private extern static void aiui_agent_destroy(IntPtr agent);
68 |
69 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
70 | private extern static bool aiui_set_msc_dir(IntPtr szDir);
71 |
72 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
73 | private extern static bool aiui_set_msc_cfg(IntPtr szDir);
74 |
75 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
76 | private extern static bool aiui_init_logger(IntPtr szDir);
77 |
78 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
79 | private extern static void aiui_set_log_level(LogLevel level);
80 |
81 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
82 | private extern static void aiui_set_net_log_level(LogLevel level);
83 |
84 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
85 | private extern static void aiui_set_save_data_log(bool save, int logSizeMB);
86 |
87 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
88 | private extern static bool aiui_set_data_log_dir(IntPtr szDir);
89 |
90 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
91 | private extern static void aiui_set_system_info(IntPtr key, IntPtr val);
92 |
93 | [DllImport("aiui", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
94 | private extern static bool aiui_set_raw_audio_dir(IntPtr szDir);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------