给FoloToy ChatGPT火火兔加点小功能
2023 Oct 26
See all posts
前情
家里有娃的基本上都知道火火兔,OpenAI的ChatGPT更不用说,已经火了一年多了。这两个结合起来会怎么样?还真有人做了:Folotoy
是最近很好玩的一个可以玩ChatGPT的火火兔智能硬件,9月27日从他们的B站店铺七里台技工的工房
购买了成品兔子,10月13号兔子终于到货。
用火火兔试玩了一天他们免费提供的测试服务器后,开始自己搭建服务器。值得一提的是,在这搭建的过程中,碰到问题就问Folotoy的小伙子们,他们解答问题非常专业,热情满满,而且固件和服务程序都不断更新,真是一群朝气蓬勃的人,给他们点赞!
ChatGPT使用起来对国外群众来说可能跟呼吸一样容易,但是对国内群众,就没有这么简单了:
- 买ChatGPT会员是个麻烦事,刚开始只能去各种水龙头网站上1美元1美元的蹭,好在gpt3.5便宜,蹭一美元的配额可以用很久,但终究不是长久之计,后来让身在美帝的同学毛毛帮开通了,真给力,话说他已经帮过我好多次了。
- ChatGPT屏蔽了大陆不意外,居然还屏蔽了香港,所以说一般买了ChatGPT服务也没用,好在问题总有解决办法。
这过程中发现oneapi是一个神器,另外ChatGPT-Next-Web桌面版本非常好用,体积极小,rust
tauri能做得这么好。这是最近几个月发现的两个绝佳好软件,我已经离不开了。
- 开通azure
tts服务相对容易,(2023-11-20更新:只免费一个月,继续信用卡注册后才能免费一年,嫌麻烦用edge-tts吧),然后就开始折腾各种语音角色,甚至还试了elevenlabs的multilingual,真的是强大,好玩极了。
简单说来,Folotoy
火火兔对话的原理是是三个步骤: -
1.asr语音识别为文字 - 2.文字给chatGPT,得到文字答案 -
3.文字转语音生成azure tts,
目前跟ChatGPT API的交互还是文字,如果以后OpenAI能放开multimodal
input把1,2一步到位,或者把2,3一部到位也不是不可能。
上面的每个步骤的解决方案: - 步骤1目前主要用的是OpenAI 的 Whisper -
步骤2就体现出oneapi代理的厉害之处了,可以将所有大模型厂家的api,管你是Anthropic
Claude, Google PaLM2,还是百度文心一言,智谱
ChatGLM统统都能转成和OpenAI兼容,功德无量 - 步骤3的可选更多,其中azure
tts算是非常不错的。
小需求
Folotoy火火兔的定位应该是给孩子玩的,没有屏幕(没屏幕是对的,有屏幕那不如用手机app了不是,麦克风还更好),其实用下来,发现大人比孩子更喜欢用,家里小朋友们还是在户外疯玩或者看动画片是最爱,可能大一点的小孩更喜欢用吧。
即便如此,大人还是希望能看到字幕,首先网络延迟还是有的,等的过程看看东西也不错,二来用它来学英语的话精益求精有字幕也算一个有用的选项。
实现
官方实现的话,可能会从mqtt服务器消息入手,把对话发布出去,谁愿意订阅谁订阅,但是他们现在肯定没有时间,他们应该有最高优先级的新功能开发,另外每天在群里的各种问题也够他们忙的了。
自己实现的话,只能从服务器日志入手,这样也好,解耦:
后端
代码很简单,用Go实现,一会就写完了:
package main
import (
"errors"
"fmt"
"github.com/hpcloud/tail"
"gopkg.in/antage/eventsource.v1"
"log"
http "net/http"
"os/exec"
"strings"
"time"
)
func extractContent(str, start, end string) string {
startIndex := strings.Index(str, start)
if startIndex == -1 {
return ""
}
endIndex := strings.Index(str, end)
if endIndex == -1 {
return ""
}
content := str[startIndex+len(start) : endIndex]
return strings.TrimSpace(content)
}
func getDockerLogFile(imageName string) (string, string, error) {
// 执行 docker ps 命令获取容器 ID
cmd := exec.Command("docker", "ps", "-qf", "ancestor="+imageName)
output, err := cmd.Output()
if err != nil {
s := fmt.Sprintf("Failed to execute docker ps command: %s", err)
return "", "", errors.New(s)
}
// 检查容器是否存在
containerID := strings.TrimSpace(string(output))
if containerID == "" {
s := fmt.Sprintf("No container found for image: %s", imageName)
return "", "", errors.New(s)
}
// 执行 docker inspect 命令获取容器日志文件位置
cmd = exec.Command("docker", "inspect", "--format={{.LogPath}}", containerID)
output, err = cmd.Output()
if err != nil {
s := fmt.Sprintf("Failed to execute docker inspect command: %s", err)
return "", "", errors.New(s)
}
// 检查日志文件是否存在
logFile := strings.TrimSpace(string(output))
if logFile == "" {
s := fmt.Sprintf("Log file not found for container: %s", containerID)
return "", "", errors.New(s)
}
return logFile, containerID, nil
}
func handleFoloLog(mux *http.ServeMux) {
logFilePath, _, err := getDockerLogFile("lewangdev/folotoy-server:latest")
if err != nil {
log.Fatalf(err.Error())
return
}
es := eventsource.New(nil, nil)
defer es.Close()
mux.Handle("/events", es)
config := tail.Config{
ReOpen: true, // 重新打开
Follow: true, // 是否跟随
Location: &tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件的哪个地方开始读
MustExist: false, // 文件不存在不报错
Poll: true,
}
tails, err := tail.TailFile(logFilePath, config)
if err != nil {
fmt.Println("tail file failed, err:", err)
return
}
var (
line *tail.Line
ok bool
)
for {
line, ok = <-tails.Lines
if !ok {
fmt.Printf("tail file close reopen, filename:%s\n", tails.Filename)
time.Sleep(time.Second)
continue
}
if strings.Contains(line.Text, "Transcribed:") {
content := extractContent(line.Text, "Transcribed:", "\",\"stream\"")
es.SendEventMessage("[B]:"+content, "", "")
}
if strings.Contains(line.Text, "Speech synthesized for text [") {
content := extractContent(line.Text, "Speech synthesized for text [", "]")
es.SendEventMessage("[A]:"+content, "", "")
}
}
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.FileServer(http.Dir("./public")))
go func() {
handleFoloLog(mux)
}()
go func() {
http_err := http.ListenAndServe("127.0.0.1:9999", mux)
if http_err != nil {
log.Println(http_err)
}
}()
<-make(chan struct{})
}
服务器配置
Nginx配置一个子域名,简单做一个权限控制,并加上对SSE的支持:
server {
server_name dialogs.xxx.com;
listen 443 ssl;
charset UTF-8;
auth_basic "closed site";
auth_basic_user_file some_passwd.txt;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
location / {
proxy_pass http://127.0.0.1:9999;
}
前端
前端更简单,主流浏览器都支持SSE,页面不用手动刷新新消息就出来了,用户体验一流:
<!DOCTYPE html>
<html>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta http-equiv="x-ua-compatible" content="ie=edge"/>
<link rel="icon" href="data:,">
<head>
<title>Folotoy Dialogs</title>
<script type="text/javascript">
window.addEventListener("DOMContentLoaded", function () {
var evsrc = new EventSource("/events");
evsrc.onmessage = function (ev) {
document.getElementById("log")
.insertAdjacentHTML("beforeend", "<li>" + ev.data + "</li>");
}
evsrc.onerror = function (ev) {
console.log("readyState = " + ev.currentTarget.readyState);
}
})
</script>
</head>
<body>
<h1>Dialogs:</h1>
<div>
<ul id="log">
</ul>
</div>
</body>
</html>
效果
娃在家里跟火火兔说话的时候,大人无论何时何地,用电脑或手机上打开这个网页,就可以实时看到对话的文字版,体验非常棒。
给FoloToy ChatGPT火火兔加点小功能
2023 Oct 26 See all posts前情
家里有娃的基本上都知道火火兔,OpenAI的ChatGPT更不用说,已经火了一年多了。这两个结合起来会怎么样?还真有人做了:
Folotoy
是最近很好玩的一个可以玩ChatGPT的火火兔智能硬件,9月27日从他们的B站店铺七里台技工的工房
购买了成品兔子,10月13号兔子终于到货。用火火兔试玩了一天他们免费提供的测试服务器后,开始自己搭建服务器。值得一提的是,在这搭建的过程中,碰到问题就问Folotoy的小伙子们,他们解答问题非常专业,热情满满,而且固件和服务程序都不断更新,真是一群朝气蓬勃的人,给他们点赞!
ChatGPT使用起来对国外群众来说可能跟呼吸一样容易,但是对国内群众,就没有这么简单了:
简单说来,
Folotoy
火火兔对话的原理是是三个步骤: - 1.asr语音识别为文字 - 2.文字给chatGPT,得到文字答案 - 3.文字转语音生成azure tts,目前跟ChatGPT API的交互还是文字,如果以后OpenAI能放开multimodal input把1,2一步到位,或者把2,3一部到位也不是不可能。
上面的每个步骤的解决方案: - 步骤1目前主要用的是OpenAI 的 Whisper - 步骤2就体现出oneapi代理的厉害之处了,可以将所有大模型厂家的api,管你是Anthropic Claude, Google PaLM2,还是百度文心一言,智谱 ChatGLM统统都能转成和OpenAI兼容,功德无量 - 步骤3的可选更多,其中azure tts算是非常不错的。
小需求
Folotoy火火兔的定位应该是给孩子玩的,没有屏幕(没屏幕是对的,有屏幕那不如用手机app了不是,麦克风还更好),其实用下来,发现大人比孩子更喜欢用,家里小朋友们还是在户外疯玩或者看动画片是最爱,可能大一点的小孩更喜欢用吧。
即便如此,大人还是希望能看到字幕,首先网络延迟还是有的,等的过程看看东西也不错,二来用它来学英语的话精益求精有字幕也算一个有用的选项。
实现
官方实现的话,可能会从mqtt服务器消息入手,把对话发布出去,谁愿意订阅谁订阅,但是他们现在肯定没有时间,他们应该有最高优先级的新功能开发,另外每天在群里的各种问题也够他们忙的了。
自己实现的话,只能从服务器日志入手,这样也好,解耦:
先把folotoy-sever的
LOG_LEVEL: INFO
改成LOG_LEVEL: DEBUG
然后实时tail日志,找到相关的文本,用SSE(server sent events)把文字向浏览器推出去,这应该是使用SSE的一个绝佳场景。
后端
代码很简单,用Go实现,一会就写完了:
服务器配置
Nginx配置一个子域名,简单做一个权限控制,并加上对SSE的支持:
前端
前端更简单,主流浏览器都支持SSE,页面不用手动刷新新消息就出来了,用户体验一流:
效果
娃在家里跟火火兔说话的时候,大人无论何时何地,用电脑或手机上打开这个网页,就可以实时看到对话的文字版,体验非常棒。