Skip to content

Commit

Permalink
新增GPT Vision支持,支持拍照、图库、分享;主界面添加模型选框;允许自定义多个模型;重构对话消息相关逻辑;优化部分界面;版本号修改…
Browse files Browse the repository at this point in the history
…为1.7.0
  • Loading branch information
Skythinker616 committed Dec 4, 2023
1 parent 0c42bf2 commit 9e2ca31
Show file tree
Hide file tree
Showing 21 changed files with 794 additions and 139 deletions.
17 changes: 17 additions & 0 deletions .idea/deploymentTargetDropDown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 32 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<div align=center>
<font size=3>
<b>国内可用 · 免费使用 · 语音交互 · 支持联网</b>
<b>免费聊天 · 语音交互 · 支持联网 · 支持识图</b>
</font>
</div>

Expand All @@ -27,8 +27,6 @@
</p>
</div>



---

## 介绍
Expand All @@ -37,6 +35,7 @@

- 支持用户预设**问题模板**,支持**连续对话**,支持`gpt-3.5-turbo``gpt-4`等模型
- **支持联网**,允许GPT获取在线网页
- 支持拍照或从相册中**上传图片**到GPT Vision模型
- 通过无障碍功能捕获音量键事件,实现在**任意界面唤起**
- 支持从**全局上下文菜单**(选中文本后弹出的系统菜单)中直接唤起
- 支持通过状态栏**快捷按钮**唤起
Expand Down Expand Up @@ -100,13 +99,25 @@

**四、支持连续对话**

激活上方的对话图标,即可保留当前会话,进行连续对话
激活上方的对话图标,即可保留当前会话,进行连续对话(点击左侧的头像图标可以对单条对话进行删除、重试等操作)

<div align="center">
<img src="readme_img/multi_chat.gif" height="400px">
</div>

**五、支持GPT联网**
**五、支持上传图片到Vision**

当选择的模型中含有`vision`时(如`gpt-4-vision-preview`),输入框左侧会出现图片按钮,点击后可以拍照或从相册中选择图片

从其他应用中分享图片时,也可以选择本程序,将图片添加到输入框

<div align="center">
<img src="readme_img/vision.gif" height="400px">
</div>

> 注:Vision模型一般无法免费使用(如Chatanywhere),有需要的用户可以考虑付费服务
**六、支持GPT联网**

本程序实现了OpenAI的Function接口,允许GPT发起联网请求,程序会向GPT自动返回所需的网页数据,使GPT具有联网能力(需先在设置中开启联网选项)

Expand All @@ -120,8 +131,10 @@
</div>

> 注1:上图均为使用`gpt-3.5-turbo`模型的测试结果,建议在提问前加入“百度搜索”、“在线获取”、“从xxx获取”等字样引导GPT,以获得更好的联网效果
>
> 注2:由于需要将网页内容发送给GPT,联网时会产生大量Token消耗,`gpt-4`模型请谨慎使用
>
> 注3:`gpt-4-vision-preview`模型暂不支持联网
---

Expand Down Expand Up @@ -211,6 +224,10 @@ A: 网页加载超时(15s)、需要登录、需要验证等原因都可能导致

### 其他问题

**Q: 为什么列表中没有我需要的模型?**

A: 软件仅内置了少数常用模型,你可以在设置中添加自定义模型(以英文分号分隔),添加后即会出现在列表中

**Q: GPT返回的内容中表格和图片无法正常显示?**

A: 所使用的Markdown渲染器无法在测试中产生稳定的结果,因此暂不支持表格和图片
Expand All @@ -227,12 +244,13 @@ A: 排除网络因素,该错误一般由OpenAI接口产生,可能由于其
- **2023.09.13** 支持连续对话、GPT-4、百度长语音识别,上下文菜单唤起
- **2023.10.06** 添加华为HMS语音识别
- **2023.11.06** 添加联网功能
- **2023.12.04** 添加Vision识图功能

---

## TODO

- 对话保存功能、删除部分对话功能
- 保存对话功能
- 支持渲染Markdown表格、图片等
- 允许GPT控制手机其他功能

Expand All @@ -244,13 +262,14 @@ A: 排除网络因素,该错误一般由OpenAI接口产生,可能由于其

| 机型 | 系统版本 | Android 版本 | 本程序版本 |
| :--: | :-----: | :----------: | :-------: |
| 荣耀 7C | EMUI 8.0.0 | Android 8 | 1.6.1 |
| 荣耀 20 | HarmonyOS 3.0.0 | Android 10 | 1.6.1 |
| 华为 Mate 30 | HarmonyOS 3.0.0 | Android 12 | 1.4.0 |
| 荣耀 7C | EMUI 8.0.0 | Android 8 | 1.7.0 |
| 荣耀 20 | HarmonyOS 3.0.0 | Android 10 | 1.7.0 |
| 华为 Mate 30 | HarmonyOS 3.0.0 | Android 12 | 1.6.0 |
| 华为 Mate 30 | HarmonyOS 4.0 | Android 12 | 1.7.0 |
| 荣耀 Magic 4 | MagicOS 7.0 | Android 13 | 1.2.0 |
| 红米 K20 Pro | MIUI 12.5.6 | Android 11 | 1.5.0 |
| 红米 K60 Pro | MIUI 14.0.23 | Android 13 | 1.6.0 |
| Pixel 2 (模拟器) | Android 12 | Android 12 | 1.2.0 |
| 红米 K60 Pro | MIUI 14.0.23 | Android 13 | 1.7.0 |
| Pixel 2 (模拟器) | Android 12 | Android 12 | 1.7.0 |

---

Expand All @@ -262,7 +281,7 @@ A: 排除网络因素,该错误一般由OpenAI接口产生,可能由于其

## 隐私说明

本程序不会以任何方式收集用户的个人信息,语音输入会直接发送给华为或百度API,提问会直接发送给OpenAI API,不会经过任何中间服务器
本程序不会以任何方式收集用户的个人信息,语音输入会直接发送给华为或百度API,提问会直接发送给OpenAI API,不会经过其他中间服务器

---

Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ android {
minSdk 26
targetSdk 32
versionCode 1
versionName "1.6.2"
versionName "1.7.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -57,6 +57,6 @@ dependencies {
implementation"io.noties.markwon:linkify:$markwon_version"
implementation"io.noties.markwon:syntax-highlight:$markwon_version"
annotationProcessor 'io.noties:prism4j-bundler:2.0.0'
implementation group: 'com.unfbx', name: 'chatgpt-java', version: '1.1.0'
implementation group: 'com.unfbx', name: 'chatgpt-java', version: '1.1.3'
implementation 'com.huawei.hms:ml-computer-voice-asr:3.12.0.301'
}
19 changes: 18 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,29 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<intent-filter
android:label="询问 GPT">
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter
android:label="发送到Vision">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
</application>

</manifest>
150 changes: 104 additions & 46 deletions app/src/main/java/com/skythinker/gptassistant/ChatApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
import androidx.annotation.Nullable;

import com.unfbx.chatgpt.OpenAiStreamClient;
import com.unfbx.chatgpt.entity.chat.BaseChatCompletion;
import com.unfbx.chatgpt.entity.chat.ChatCompletionWithPicture;
import com.unfbx.chatgpt.entity.chat.Content;
import com.unfbx.chatgpt.entity.chat.FunctionCall;
import com.unfbx.chatgpt.entity.chat.Functions;
import com.unfbx.chatgpt.entity.chat.ImageUrl;
import com.unfbx.chatgpt.entity.chat.Message;
import com.unfbx.chatgpt.entity.chat.ChatCompletion;
import com.unfbx.chatgpt.entity.chat.MessagePicture;
import com.unfbx.chatgpt.entity.chat.Parameters;

import java.io.IOException;
Expand Down Expand Up @@ -39,6 +44,28 @@ public enum ChatRole {
FUNCTION
}

public static class ChatMessage {
public ChatRole role;
public String contentText;
public String contentImageBase64;
public String functionName;
public ChatMessage(ChatRole role) {
this.role = role;
}
public ChatMessage setText(String text) {
this.contentText = text;
return this;
}
public ChatMessage setImage(String base64) {
this.contentImageBase64 = base64;
return this;
}
public ChatMessage setFunction(String name) {
this.functionName = name;
return this;
}
}

String url = "";
String apiKey = "";
String model = "";
Expand All @@ -63,19 +90,19 @@ public ChatApiClient(String url, String apiKey, String model, OnReceiveListener
setApiInfo(url, apiKey);
}

public void sendPrompt(String systemPrompt, String userPrompt) {
if(systemPrompt == null && userPrompt == null) {
listener.onError("模板和问题内容均为空");
return;
}

sendPromptList(Arrays.asList(
Pair.create(ChatRole.SYSTEM, systemPrompt),
Pair.create(ChatRole.USER, userPrompt)
));
}
// public void sendPrompt(String systemPrompt, String userPrompt) {
// if(systemPrompt == null && userPrompt == null) {
// listener.onError("模板和问题内容均为空");
// return;
// }
//
// sendPromptList(Arrays.asList(
// Pair.create(ChatRole.SYSTEM, systemPrompt),
// Pair.create(ChatRole.USER, userPrompt)
// ));
// }

public void sendPromptList(List<Pair<ChatRole, String>> promptList) {
public void sendPromptList(List<ChatMessage> promptList) {
if(url.isEmpty()) {
listener.onError("请在设置中填写服务器地址");
return;
Expand All @@ -87,44 +114,75 @@ public void sendPromptList(List<Pair<ChatRole, String>> promptList) {
return;
}

ArrayList<Message> messages = new ArrayList<>();
for(Pair<ChatRole, String> prompt : promptList) {
if(prompt.first == ChatRole.SYSTEM) {
messages.add(Message.builder().role(Message.Role.SYSTEM).content(prompt.second).build());
} else if(prompt.first == ChatRole.USER) {
messages.add(Message.builder().role(Message.Role.USER).content(prompt.second).build());
} else if(prompt.first == ChatRole.ASSISTANT) {
if(prompt.second.startsWith("[Function]")) {
int sepIndex = prompt.second.indexOf("\n");
String funcName = prompt.second.substring("[Function]".length(), sepIndex);
String arguments = prompt.second.substring(sepIndex + 1);
FunctionCall functionCall = FunctionCall.builder()
.name(funcName)
.arguments(arguments)
.build();
messages.add(Message.builder().role(Message.Role.ASSISTANT).functionCall(functionCall).build());
} else {
messages.add(Message.builder().role(Message.Role.ASSISTANT).content(prompt.second).build());
BaseChatCompletion chatCompletion = null;

if(!model.contains("vision")) {
ArrayList<Message> messageList = new ArrayList<>();
for (ChatMessage message : promptList) {
if (message.role == ChatRole.SYSTEM) {
messageList.add(Message.builder().role(Message.Role.SYSTEM).content(message.contentText).build());
} else if (message.role == ChatRole.USER) {
messageList.add(Message.builder().role(Message.Role.USER).content(message.contentText).build());
} else if (message.role == ChatRole.ASSISTANT) {
if (message.functionName != null) {
FunctionCall functionCall = FunctionCall.builder()
.name(message.functionName)
.arguments(message.contentText)
.build();
messageList.add(Message.builder().role(Message.Role.ASSISTANT).functionCall(functionCall).build());
} else {
messageList.add(Message.builder().role(Message.Role.ASSISTANT).content(message.contentText).build());
}
} else if (message.role == ChatRole.FUNCTION) {
messageList.add(Message.builder().role(Message.Role.FUNCTION).name(message.functionName).content(message.contentText).build());
}
} else if(prompt.first == ChatRole.FUNCTION) {
int sepIndex = prompt.second.indexOf("\n");
String funcName = prompt.second.substring(0, sepIndex);
String reply = prompt.second.substring(sepIndex + 1);
messages.add(Message.builder().role(Message.Role.FUNCTION).name(funcName).content(reply).build());
}
}

ChatCompletion chatCompletion;
if(!functions.isEmpty()) {
chatCompletion = ChatCompletion.builder()
.messages(messages)
.model(model)
.functions(functions)
.functionCall("auto")
.build();
if (!functions.isEmpty()) {
chatCompletion = ChatCompletion.builder()
.messages(messageList)
.model(model)
.functions(functions)
.functionCall("auto")
.build();
} else {
chatCompletion = ChatCompletion.builder()
.messages(messageList)
.model(model)
.build();
}
} else {
chatCompletion = ChatCompletion.builder()
.messages(messages)
ArrayList<MessagePicture> messageList = new ArrayList<>();
for (ChatMessage message : promptList) {
List<Content> contentList = new ArrayList<>();
if (message.contentText != null) {
contentList.add(Content.builder().type(Content.Type.TEXT.getName()).text(message.contentText).build());
}
if(message.contentImageBase64 != null) {
ImageUrl imageUrl = ImageUrl.builder().url("data:image/jpeg;base64," + message.contentImageBase64).build();
contentList.add(Content.builder().type(Content.Type.IMAGE_URL.getName()).imageUrl(imageUrl).build());
}
if (message.role == ChatRole.SYSTEM) {
messageList.add(MessagePicture.builder().role(Message.Role.SYSTEM).content(contentList).build());
} else if (message.role == ChatRole.USER) {
messageList.add(MessagePicture.builder().role(Message.Role.USER).content(contentList).build());
} else if (message.role == ChatRole.ASSISTANT) {
if (message.functionName != null) {
FunctionCall functionCall = FunctionCall.builder()
.name(message.functionName)
.arguments(message.contentText)
.build();
messageList.add(MessagePicture.builder().role(Message.Role.ASSISTANT).functionCall(functionCall).build());
} else {
messageList.add(MessagePicture.builder().role(Message.Role.ASSISTANT).content(contentList).build());
}
} else if (message.role == ChatRole.FUNCTION) {
messageList.add(MessagePicture.builder().role(Message.Role.FUNCTION).name(message.functionName).content(contentList).build());
}
}

chatCompletion = ChatCompletionWithPicture.builder()
.messages(messageList)
.model(model)
.build();
}
Expand Down

0 comments on commit 9e2ca31

Please sign in to comment.