2024年1月

前言

今天分享一款基于C#开源(MIT License开源协议)、免费、离线、功能齐全的Windows开发者工具箱,号称开发人员的瑞士军刀,可以帮助开发者完成日常工作开发中常用功能:DevToys。

工具介绍

DevToys是一个专门为开发者设计的Windows工具箱,完全支持离线运行,无需使用许多不真实的网站来处理你的数据,常用功能有:格式化(支持 JSON、SQL、XML)、JWT解码、URL编码/解码、UUID生成、图片压缩、文本比较、正则表达式测试、Markdown预览等28+种实用工具。

支持系统

DevToys需要在 Windows 10 build 1903+ 或更高版本中使用。

工具源代码

工具下载

微软应用商店:

https://apps.microsoft.com/detail/9PGCV4V3BK4W?rtc=1&hl=zh-cn&gl=CN

GitHub下载:

https://github.com/veler/DevToys/releases

部分功能截图

项目源码地址



更多项目实用功能和特性欢迎前往项目开源地址查看

我们是
袋鼠云数栈 UED 团队
,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:修能

生产力工具 + AI 是不可逆转的趋势,慢慢的大模型能力通过 AI Agent 落地的工程化能力也开始趋于成熟。作为大数据产品的数栈也必然是需要借助 AI 能力提升产品竞争力。
去年 12 月,我们在产品中上线了 AI+ 的功能,借助已经开源的大模型的能力,帮助我们探索和落地更多地应用场景。在初版 AI+ 的功能中,我们实现了基础功能的通话。

SSE

在 ChatGPT 中,我们在等待大模型生成回答的时间通常不需要很久。这是因为 ChatGPT 通过
server-sent events(SSE)
来实现将生成的部分回答通过事件流传递到前端。而这就让前端不必等回答全部生成后再获取,也就使得不需要请求等待很久。

SSE 是一种基于 HTTP 协议的单向通信机制,用于服务端向客户端推送数据。

SSE WebSocket
基于 HTTP 协议 基于 TCP 连接,本身是一种协议
单向通信 双向通信
简单易用 复杂

入门使用

// 创建 SSE 的实例
const evtSource = new EventSource("//api.example.com/ssedemo.php", {
  withCredentials: true,
});

// 添加监听事件
evtSource.onmessage = (event) => {
  const newElement = document.createElement("li");
  const eventList = document.getElementById("list");

  newElement.textContent = `message: ${event.data}`;
  eventList.appendChild(newElement);
};

// 错误处理
evtSource.onerror = (err) => {
  console.error("EventSource failed:", err);
};

// 关闭事件流
evtSource.close();

需要注意的是,SSE 请求的服务端响应信息头的 MIME 类型必须是
text/event-stream
,否则会无法监听到事件。
另外,由于是基于 HTTP 协议的,所以在 HTTP/1.1 或更低的时候,会受浏览器最大连接数的限制。


Fields

收到的消息格式一定是具有以下字段的某种组合,其他字段名都将忽略,
每行
一个:

  • event
  • data
  • id
  • retry
: this is a test stream // 第一条消息,这会被解析会注释

data: some text // 第二条消息

data: another message // 第三条消息
data: with two lines

event: userconnect // 第四条消息
data: {"username": "bobby", "time": "02:33:48"}


如上所示,默认浏览器的 EventSource API 虽然可用,但是限制比较多。

  1. 只支持 url 和 withCredentials 参数。不支持往 body 里传参数。而通常来说 URL 是有最大长度限制的。
  2. 无法自定义请求头。
  3. 只能发起 GET 请求。


其实,我们也可以通过 Fetch 来实现 SSE 的通信,只不过需要额外自行处理数据流的传递。

实现

首先,我们借助 Fetch 的能力来实现请求。

const response = await fetch(url, options);

通过接受用户提供的 url 和 options 发起一个 fetch 的请求。
然后,我们需要排除掉非 SSE 的请求类型,我们可以直接拿响应的 header 中拿
content-type
进行判断。

const contentType = response.headers.get('content-type');
if (!contentType?.startsWith('text/event-stream')) {
    throw new Error('SSE 请求必须设置 content-type 为 text/event-stream');
}

接着,我们业务场景中通常直接通过
response.json()
获取 JSON 格式的数据了,但这里我们由于是事件流,所以我们通过
response.body
拿到的是一个
ReadableStream
。我们需要借助相关的 API 进行流的读取。

const reader = response.body.getReader();
let result: ReadableStreamDefaultReadResult<Uint8Array>;
while (!(result = await reader.read()).done) {
  	// 假定每一次 read 的 value 都是完整的消息
    onmessage(onChunk(result.value));
}

其中 onChunk 函数就是处理事件流中的每一份数据的。

// 伪代码
function onChunk(arr: Uint8Array){
  const links = seekLinks();
  // 待完善
}

在实现
seekLinks
方法之前,我们需要先知道到什么时候算每一行的结束。


从 Fields 可以知道,每一行是以
\n
作为区分的。

function seekLinks(arr: Uint8Array){
  const lines = [];
  const buffer = arr;
  const bufLength = buffer.length;
  let position = 0;
  let lineStart = 0;
  while(position < bufLength){
    // '\n'.charCodeAt() === 10;
    if(buffer[position] === 10){
      lines.push(buffer.slice(lineStart, position));
      lineStart = position;
    };
    position += 1;
  }
  return lines;
}


在获取到所有行后,针对每一行做处理。

// 伪代码
function onChunk(arr: Uint8Array){
  const links = seekLinks();
  const decoder = new TextDecoder();
  let message = {
    data: '',
    event: '',
    id: '',
    retry: undefined,
  }:
  links.forEach((line) => {
    // ':'.charCodeAt() === 58;
    const colon = line.findIndex(l => l === 58);
    const fieldArr = line.slice(0, colon);
    const valueArr = line.slice(colon);
    if(colon === -1){
      // 当冒号作为开头的时候,解析成注释
      return;
    }
    const field = decoder.decode(fieldArr);
    const value = decoder.decode(valueArr);
    switch (field) {
      case 'data':
          message.data = message.data
              ? message.data + '\n' + value
              : value;
          break;
      case 'event':
          message.event = value;
          break;
      case 'id':
          message.id = value;
          break;
      case 'retry':
          const retry = parseInt(value, 10);
          message.retry = retry
          break;
  	}
  });
  return message;
}

大致完成了最简单的基础功能的解析,而以上伪代码参考
fetch-event-source
的源码。


借助 fetch-event-source 的能力,在数栈产品中调用的方式和 HTTP 请求基本保持一致。

function sse(url: string, params: any, options: FetchEventSourceInit) {
  const headers = {
    'Content-Type': 'application/json',
    accept: 'text/event-stream',
  };
  fetchEventSource(url, {
    method: 'POST',
    body: JSON.stringify(params),
    headers,
    ...options,
  });
}

打字机效果

接着,我们实现具备科技感的打字机效果:

输出

这里我们不能直接将响应的消息直接打印到屏幕上,因为响应的消息通常是好多字,这样子会导致打字机效果显得非常卡顿,用户体验不佳。
在数栈产品中,我们通过将响应的消息收集到暂存区中,然后通过每秒从暂存区中取出若干个字符打印到屏幕上,优化打字机卡顿的效果。

function AIGC(){
   const typing = useTyping({
      // 暂存区启动后,每个 delay 的时间都会执行该方法将消息打印到屏幕上
      onTyping(val) {
        // ...
      },
  });
	const handleChat = (message: string) => {
      // 标志暂存区需要开始存响应的消息了
      typing.start();
      requestChat(params, {
        onmessage(event: { data: string }) {
           	const { data } = event;
            // 把响应的消息存入暂存区中
            typing.push(data);
        },
        onclose() {
            // 关闭或失败的话,释放暂存区的数据
            typing.close();
        },
        onerror() {
            typing.close();
        },
    });
  };
}

其中,相关暂存区的代码整理成
useTyping
实现。

export default function useTyping({
    onTyping,
    onEnd,
}: {
    onTyping: (val: string) => void;
    onEnd: () => void;
}) {
    const interval = useRef<number>();
    const queue = useRef<string>('');
    const isStart = useRef<boolean>(false);

    function startTyping() {
        if (interval.current) return;
        let index = 0;
        interval.current = window.setInterval(() => {
            if (index < queue.current.length) {
                const str = queue.current;
                onTyping(str.slice(0, index + 1));
                index++;
            } else if (!isStart.current) {
                // 如果发送了全部的消息且信号关闭,则清空队列
                window.clearInterval(interval.current);
                interval.current = 0;
                onEnd();
            }
            // 如果发送了全部的消息,但是信号没有关闭,则什么都不做继续轮训等待新的消息
        }, 50);
    }

    useEffect(() => {
        return () => {
            window.clearInterval(interval.current);
            interval.current = 0;
        };
    }, []);

    function start() {
        isStart.current = true;
        window.clearInterval(interval.current);
        interval.current = 0;
        queue.current = '';
    }

    function push(str: string) {
        if (!isStart.current) return;
        queue.current += str.replace(/\\n/g, '\n');
        startTyping();
    }

    // 关闭的时候不需要清空队列,因为可能还有一些消息没有发送完毕,统一等消息发送完毕后关闭
    function close() {
        isStart.current = false;
    }

    return { start, push, close };
}

光标

在实现了打字机效果后,我们还需要添加一个闪烁的光标。
原理比较简单,就是在消息区域的最后一个元素的末尾添加元素即可。

.markdown {
  >*:last-child::after {
    content: " ";
    width: 2px;
    height: 13px;
    transform: translate(1px, 2px);
    font-family: Menlo, Monaco, "Courier New", monospace;
    font-weight: normal;
    font-size: 0;
    font-feature-settings: "liga" 0, "calt" 0;
    line-height: 13px;
    letter-spacing: 0;
    display: inline-block;
    visibility: hidden;
    animation: blinker 1s step-end infinite;
    background: #000;
  }

  @keyframes blinker {
    0% {
      visibility: inherit;
    }
    50% {
      visibility: hidden;
    }
    100% {
      visibility: inherit;
    }
  }
}

当然,这里有一些问题,在 markdown 解析出 Code Block 的时候会导致光标错位,这个问题 ChatGPT 同样也有。


那么到这里,我们就实现了一个具备基础功能的 AI+ 的需求。

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

前言

GC的全称是Garbage Collection也就是
垃圾回收
的意思,在PHP中,是使用引用计数和回收周期来自动管理内存对象的,当一个对象被设置为NULL,或者没有任何指针指向时,他就会变成垃圾,被GC机制回收掉。

环境配置

php.ini终配置好xdebug,
xdebug_debug_zval
是用来查看容器变量内容的函数

<?php 
$a = "F12";
xdebug_debug_zval("a");
?>


在PHP GC机制中,当程序终止时就会让变量的
refcount
减1,如果
refcount-1
为0的话,就会销毁回收该变量

引用计数

is_ref
表示该变量是否被引用,操作系统学的好的同学应该很容易理解该内容

<?php 
  $a = "F12";
  $b = &$a;
  xdebug_debug_zval("a");
?>

# 运行结果
a: (refcount=2, is_ref=1)='F12'

$b是$a的引用,所以
is_ref=1
,同时
refcount
也会加1,因为此时是有两个变量的(两变量指向同一个地址),所以销毁时要让
refcount
减2。
当变量是array类型时,也是一样的规则

<?php 
  $a = "F12";
  $arr = array(0=>"test", 1=>&$a);
  xdebug_debug_zval("arr");
?>
# 运行结果
arr: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='test', 1 => (refcount=2, is_ref=1)='F12')

如果我们在引用前将$a销毁会发生什么?

<?php 
  $a = "F12";
  unset($a);
  $arr = array(0=>"test", 1=>&$a);
  xdebug_debug_zval("a");
  xdebug_debug_zval("arr");
?>
# 运行结果
a: (refcount=2, is_ref=1)=NULL
arr: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='test', 1 => (refcount=2, is_ref=1)=NULL)
<?php 
  $a = "F12";
  $arr = array(0=>"test", 1=>&$a);
  unset($a);
  xdebug_debug_zval("a");
  xdebug_debug_zval("arr");
?>
# 运行结果
a: no such symbol
arr: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='test', 1 => (refcount=1, is_ref=1)='F12')

第一种情况,$a没有被销毁,因为在之后又引用了$a,所以$a只是指向了一个NULL,第二种情况就把$a销毁了

PHP GC在反序列化中的使用

一个简单的demo

<?php
class gc{
    public $num;
    public function __construct($num)
    {
        $this->num=$num;
        echo "construct(".$num.")"."\n";
    }
    public function __destruct()
    {
        echo "destruct(".$this->num.")"."\n";
    }
}
$a=new gc(1);
$b=new gc(2);
$c=new gc(3);

# 运行结果
construct(1)
construct(2)
construct(3)
destruct(3)
destruct(2)
destruct(1)

先创建的对象最后销毁,看看变量的内容情况:

可以看到
refcount
为1,所以当程序结束时,减1就会被回收
如果我们不把new的gc对象赋值给$a会怎样?

<?php
class gc{
    public $num;
    public function __construct($num)
    {
        $this->num=$num;
        echo "construct(".$num.")"."\n";
    }
    public function __destruct()
    {
        echo "destruct(".$this->num.")"."\n";
    }
}
new gc(1);
$b=new gc(2);
$c=new gc(3);

# 运行结果
construct(1)
destruct(1)
construct(2)
construct(3)
destruct(3)
destruct(2)

可以看到第一个gc对象,创建完就被回收了,因为没被其它变量引用,它的
refcount
一开始就是0,所以直接被回收

绕过Exception异常

思路一

一个简单的demo:

<?php
class gc{
    public $num;
    public function __construct($num)
    {
        $this->num=$num;
    }
    public function __destruct()
    {
        echo "Hello World!";
    }
}
$a = new gc(1);
$ser = serialize($a);
$b = unserialize($ser);
throw new Exception("F12 is bad");

正常来说会输出一个
Hello World!
,但是因为触发了异常,所以对象并没有被回收

我们修改一下代码:

<?php
class gc{
    public $num;
    public function __construct($num)
    {
        $this->num=$num;
    }
    public function __destruct()
    {
        echo "Hello World!";
    }
}
$a = array(0=>new gc(1),1=>1);
$ser = serialize($a);
echo $ser;
$ser = 'a:2:{i:0;O:2:"gc":1:{s:3:"num";i:1;}i:0;i:1;}';
$b = unserialize($ser);
throw new Exception("F12 is bad");

这里我们我们修改序列化的内容,将$a[0]随便指向谁,从而使new的gc对象没有引用的变量,所以触发提前回收,跟上面举的直接new gc,并不赋值是一个道理

思路二

这种方法更加简单粗暴,我们只需要让序列化的数据出错,那么当反序列化时出错时,也会让该对象提前回收

<?php
class gc{
    public $num;
    public function __construct($num)
    {
        $this->num=$num;
    }
    public function __destruct()
    {
        echo "Hello World!";
    }
}
$a = new gc(1);
$ser = serialize($a);
echo $ser;
$ser = 'O:2:"gc":1:{s:3:"num";i:1;';
$b = unserialize($ser);
throw new Exception("F12 is bad");

这里我们删去一个
}
,依然输出了
Hello World!

阿里的FunAsr对Whisper中文领域的转写能力造成了一定的挑战,但实际上,Whisper的使用者完全可以针对中文的语音做一些优化的措施,换句话说,Whisper的“默认”形态可能在中文领域斗不过FunAsr,但是经过中文特殊优化的Whisper就未必了。

中文文本标注优化

Whisper经常被人诟病的一点是对中文语音转写后标点符号的支持不够完备。首先安装whisper:

pip install -U openai-whisper

编写转写脚本:

import whisper  
device = "cuda:0" if torch.cuda.is_available() else "cpu"
audio = whisper.load_audio(audio_path)  
audio = whisper.pad_or_trim(audio)

model = whisper.load_model("large-v2",download_root="./whisper_model/")

mel = whisper.log_mel_spectrogram(audio).to(model.device)

options = whisper.DecodingOptions(beam_size=5)

result = whisper.decode(model, mel, options)  
print(result.text) 

程序返回:

Erwin_0.wav|Erwin|ZH|如果这个作战顺利。  
Erwin_1.wav|Erwin|ZH|你也许可以趁此机会干掉狩之巨人  
Erwin_10.wav|Erwin|ZH|如果到時候我不衝在最前面  
Erwin_11.wav|Erwin|ZH|他们根本不会往前冲然后我会第一个去死  
Erwin_12.wav|Erwin|ZH|地下室里到底有什么  
Erwin_13.wav|Erwin|ZH|也就无从知晓了好想去地下室看一看我之所以能撑着走到今天  
Erwin_14.wav|Erwin|ZH|就是因为相信这一天的到来。  
Erwin_15.wav|Erwin|ZH|因为艰辛着  
Erwin_16.wav|Erwin|ZH|我才想能够得到证实  
Erwin_17.wav|Erwin|ZH|我之前無數次的想過,要不然乾脆死了算了。  
Erwin_18.wav|Erwin|ZH|可即便如此,我還是想要實現父親的夢想。  
Erwin_19.wav|Erwin|ZH|然而现在  
Erwin_2.wav|Erwin|ZH|但得拿所有新兵不管選擇哪條路  
Erwin_20.wav|Erwin|ZH|她的答案就在我触手可及的地方  
Erwin_21.wav|Erwin|ZH|仅在咫尺死去的同伴们也是如此吗  
Erwin_22.wav|Erwin|ZH|那些流血的棲身,都是沒有意義的嗎?  
Erwin_23.wav|Erwin|ZH|不!不對!  
Erwin_24.wav|Erwin|ZH|那些死去士兵的意义将由我们来赋予  
Erwin_25.wav|Erwin|ZH|那些勇敢的死者可憐的死者  
Erwin_26.wav|Erwin|ZH|是他们的牺牲换来了我们活着的今天  
Erwin_27.wav|Erwin|ZH|让我们能站在这里否则今天我们将会死去  
Erwin_28.wav|Erwin|ZH|将依依托福给下一个活着的人  
Erwin_29.wav|Erwin|ZH|这就是我们与这个残酷的世界  
Erwin_3.wav|Erwin|ZH|我们基本都会死吧是的全灭的可能性相当的高  
Erwin_30.wav|Erwin|ZH|抗爭的意義  
Erwin_4.wav|Erwin|ZH|但事到如今,也只能做好玉石俱焚的觉悟。  
Erwin_5.wav|Erwin|ZH|將一切賭在獲勝渺茫的戰術上  
Erwin_6.wav|Erwin|ZH|到了这一步  
Erwin_7.wav|Erwin|ZH|要让那些年轻人们去死  
Erwin_8.wav|Erwin|ZH|就必须像一个一流的诈骗犯一样  
Erwin_9.wav|Erwin|ZH|对他们花言巧语一番

可以看到,除了语气特别强烈的素材,大部分都没有进行标点符号的标注。

但事实上,Whisper完全可以针对中文进行标注,只需要添加对应的引导词:

options = whisper.DecodingOptions(beam_size=5,prompt="生于忧患,死于欢乐。不亦快哉!")

这里通过prompt对其进行引导,通过逗号、句号以及感叹号对文本标注,引导后的效果:

Erwin_0.wav|Erwin|ZH|如果这个作战顺利。  
Erwin_1.wav|Erwin|ZH|你也许可以趁此机会干掉受之虚人。  
Erwin_10.wav|Erwin|ZH|如果到时候我不冲在最前面  
Erwin_11.wav|Erwin|ZH|他们根本不会往前冲,然后我会第一个去死。  
Erwin_12.wav|Erwin|ZH|地下室里到底有什么?  
Erwin_13.wav|Erwin|ZH|好想去地下室看一看,我之所以能撑着走到今天。  
Erwin_14.wav|Erwin|ZH|就是因为相信这一天的到来。  
Erwin_15.wav|Erwin|ZH|因为艰辛着D  
Erwin_16.wav|Erwin|ZH|我的猜想能够得到证实。  
Erwin_17.wav|Erwin|ZH|我之前无数次地想过,要不然干脆死了算了。  
Erwin_18.wav|Erwin|ZH|可即便如此,我还是想要实现父亲的梦想。  
Erwin_19.wav|Erwin|ZH|然而现在  
Erwin_2.wav|Erwin|ZH|但得拿所有新兵,不管选择哪条路。  
Erwin_20.wav|Erwin|ZH|他的答案就在我触手可及的地方。  
Erwin_21.wav|Erwin|ZH|竟在咫尺。死去的同伴们也是如此吗?  
Erwin_22.wav|Erwin|ZH|那些流血的牺牲,都是没有意义的吗?  
Erwin_23.wav|Erwin|ZH|不!不对!  
Erwin_24.wav|Erwin|ZH|那些死去士兵的意义将由我们来赋予!  
Erwin_25.wav|Erwin|ZH|那些勇敢的死者,可怜的死者!  
Erwin_26.wav|Erwin|ZH|是他们的牺牲换来了我们活着的今天!  
Erwin_27.wav|Erwin|ZH|让我们能站在这里,而今天我们将会死去!  
Erwin_28.wav|Erwin|ZH|将依依托福给下一个活着的人!  
Erwin_29.wav|Erwin|ZH|这就是我们与这个残酷的世界。  
Erwin_3.wav|Erwin|ZH|是的,全灭的可能性相当的高。  
Erwin_30.wav|Erwin|ZH|抗争的意义!  
Erwin_4.wav|Erwin|ZH|但事到如今,也只能做好玉石俱焚的觉悟。  
Erwin_5.wav|Erwin|ZH|将一切赌在获胜渺茫的战术上。  
Erwin_6.wav|Erwin|ZH|到了这一步  
Erwin_7.wav|Erwin|ZH|要让那些年轻人们去死。  
Erwin_8.wav|Erwin|ZH|就必须像一个一流的诈骗犯一样。  
Erwin_9.wav|Erwin|ZH|对他们花言巧语一番。

通过transformers来调用中文模型

transformers是一个用于自然语言处理(NLP)的开源库,由Hugging Face开发和维护。它提供了各种预训练的模型,包括文本生成、文本分类、命名实体识别等多种NLP任务的模型。transformers库基于Transformer模型架构,这是一种用于处理序列数据的深度学习模型。Transformer模型在NLP领域取得了巨大成功,因为它能够处理长距离依赖关系,并且在各种NLP任务上取得了优异的性能。

使用transformers库,开发人员可以轻松地访问和使用各种预训练的NLP模型,也可以使用该库进行模型的微调和训练。transformers库支持多种主流深度学习框架,包括PyTorch和TensorFlow。

首先安装transformers:

pip install -U transformers

编写转写代码:

from transformers import pipeline  
  
device = "cuda:0" if torch.cuda.is_available() else "cpu"  
  
def transcribe_bela(audio_path):  
  
    transcriber = pipeline(  
    "automatic-speech-recognition",   
    model="BELLE-2/Belle-whisper-large-v2-zh",  
    device=device  
    )  
  
    transcriber.model.config.forced_decoder_ids = (  
    transcriber.tokenizer.get_decoder_prompt_ids(  
        language="zh",   
        task="transcribe",  
    )  
    )  
  
    transcription = transcriber(audio_path)   
  
    print(transcription["text"])  
    return transcription["text"]

这里通过BELLE-2/Belle-whisper-large-v2-zh模型来进行转写,提高中文的识别准确度和效率。

这个模型是在whisper的large-v2模型上针对中文进行了微调,以增强中文语音识别能力, Belle-whisper-large-v2-zh 在中国 ASR 基准测试(包括 AISHELL1、AISHELL2、WENETSPEECH 和 HKUST)上表现出 30-70% 的相对改进。

该模型的官方地址:

https://huggingface.co/BELLE-2/Belle-whisper-large-v2-zh

当然,也不是没有缺陷,BELLE-2模型目前基于AISHELL、WENETSPEECH等数据做的微调,弱化了标点能力。

换句话说,没法通过引导词来打标,但其实也有其他解决方案,即可以基于标点模型 对转写文本加标点。比如这个方案:

https://modelscope.cn/models/damo/punc_ct-transformer_cn-en-common-vocab471067-large/summary

BELLE-2模型的作者相当热心,有问必答,这是笔者对其模型提的Issues:

https://github.com/LianjiaTech/BELLE/issues/571

现在该模型的瓶颈是,如果微调带标点的中文数据,这块开源数据相对比较少,无法进行有效的训练。

除了大模型的中文优化版本,也有针对small模型的中文优化版本:

https://huggingface.co/Jingmiao/whisper-small-chinese_base

结语

Whisper开源模型通过transformers的微调,可以将预训练模型应用于特定的中文NLP任务,从而提高模型在该任务上的性能。微调使模型能够学习适应特定任务的特征和模式,从而实现更好的效果。

在我们的SqlSugar开发框架的WPF应端中,有时候我们需要在按钮或者其他界面元素上使用一些图标,框架中我们可以使用 lepoco/wpfui 项目的图标库,也可以使用Font-Awesome-WPF 图标库,另外我们如果喜欢阿里矢量图标库的,也可以通过使用Geometry图标来实现图标的展示,本篇随笔介绍在WPF应用中,结合阿里矢量图标库使用Geometry图标。

1、回顾lepoco/wpfui 项目的图标库,也可以使用Font-Awesome-WPF 图标库

在我们之前介绍过的随笔《
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(7) -- 图标列表展示和选择处理
》,介绍了lepoco/wpfui 项目的图标库和Font-Awesome-WPF 图标库,两种都在集成在了菜单选择上的图标设置操作中,可以根据需要选择其中一种。

1)lepoco/wpfui 项目的图标库,列表选择界面

lepoco/wpfui 项目的图标库来源于Fluent System Icons,项目地址是:
https://github.com/microsoft/fluentui-system-icons

这些图标映射到枚举对象 SymbolRegular 和 SymbolFilled,一个是常规的,一个是填充的图标。我们封装的选择图标界面如下所示。

界面展示的图标代码如下所示。

<ui:SymbolIconFontSize="48"Foreground="CornflowerBlue"Symbol="{Binding Text}"Tag="{Binding}"ToolTip="{Binding Text}" />

2)使用Font-Awesome-WPF 图标组件

在WPF中使用Font-Awesome-WPF 图标组件的很多,它的项目地址:
https://github.com/charri/Font-Awesome-WPF/blob/master/README-WPF.md

我们也可以用类似的方式来整合这个图标组件到项目中进行使用。图标选择界面运行效果如下所示,由于图标不是很多,所以一次性加载了(没有分页)。

界面展示的图标,代码如下所示。

<fa:ImageAwesomeWidth="32"HorizontalAlignment="Center"VerticalAlignment="Center"Foreground="CornflowerBlue"Icon="{Binding Text}"Tag="{Binding}" />

系统运行,动态从后端获取菜单及图标展示如下所示。

2、结合阿里矢量图标库使用Geometry图标

前面介绍了两种图标的应用方案,我们再来介绍一下Geometry图标的场景。

由于我们框架整合了HandyControl的一些组件的展示。HandyControl的控件 的

官方地址:
https://github.com/HandyOrg/HandyControl

中文文档地址:
https://handyorg.github.io/handycontrol/

通过它的控件扩展属性,我们可以很容易绑定按钮图标的展示。

部分按钮的定义如下所示。

 <ButtonMargin="5"hc:IconElement.Geometry="{StaticResource AddGeometry}"Command="{Binding EditCommand}"Content="新增"Style="{StaticResource ButtonInfo}" />
 <ButtonMargin="5"hc:IconElement.Geometry="{StaticResource t_import}"Command="{Binding ImportExcelCommand}"Content="导入Excel"Style="{StaticResource ButtonWarning}" />

通过 hc:IconElement.Geometry 的绑定,我们就可以自定义图标的展示,第一个
AddGeometry
是HandyControl的内置Geometry,而第二个
t_import
是我们用户扩展自定义导入的Geometry几何图形。

我们在项目定义一个 Geometries\Custom.xaml  文件,用来放置用户自定义的图标几何图形。

其中文件就是一个XML的文件定义。

该文件里面的集合图形,会在WPF应用的App中进行导入,如下代码所示。

<Applicationx:Class="WHC.SugarProject.WpfUI.App"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:helpers="clr-namespace:WHC.SugarProject.WpfUI.Helpers"xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"DispatcherUnhandledException="OnDispatcherUnhandledException"Exit="OnExit"Startup="OnStartup">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ui:ThemesDictionaryTheme="Dark" />
                <ui:ControlsDictionary/>
                <!--Geometries-->
<ResourceDictionary Source="/Styles/Geometries/Custom.xaml" />

                <!--HandyControl-->
                <ResourceDictionarySource="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
                <ResourceDictionarySource="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

导入文件后,这些几何图形就可以在静态资源中使用了,类似上面的代码,其中的t_import就是我们声明的图形key。

 <ButtonMargin="5"hc:IconElement.Geometry="{StaticResource t_import}"Command="{Binding ImportExcelCommand}"Content="导入Excel"Style="{StaticResource ButtonWarning}" />

为了更加直观的展示我们所有的自定义几何图标集合,我们可以通过也列表页面进行加载进行展示。动态加载所有自定义的图标集合,如下逻辑代码所示。

            var appResourceDictionary =((App)Application.Current).Resources;var mergedDictionaries =appResourceDictionary.MergedDictionaries;//指定的 source
            var source = "/Styles/Geometries/Custom.xaml";var sourceUri = newUri(source, UriKind.Relative);var specifiedDictionary = mergedDictionaries.FirstOrDefault(dictionary => dictionary.Source ==sourceUri);if (specifiedDictionary != null)
{
var sortedList =ToSortedList(specifiedDictionary.Keys);foreach (string key insortedList.Keys)
{
if (specifiedDictionary[key] isGeometry geometry)
{
this.AllItems.Add(new CListItem<Geometry>(key, geometry));
}
}
}

然后我们把它的数据整合到视图模型ViewModel中,并创建一个几何图形的显示列表界面,用来展示所有的图标显示,如下部分代码所示。

<ItemsControlx:Name="chkIcons"Grid.Row="1"Height="580"VerticalAlignment="Top"HorizontalContentAlignment="Left"VerticalContentAlignment="Top"ItemsSource="{Binding ViewModel.IconItems}"ScrollViewer.CanContentScroll="True"VirtualizingPanel.CacheLengthUnit="Item"VirtualizingStackPanel.IsVirtualizing="true"VirtualizingStackPanel.VirtualizationMode="Recycling">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanelVerticalAlignment="Top"hc:ScrollViewer.IsInertiaEnabled="True"hc:ScrollViewerAttach.Orientation="Vertical"Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <ButtonWidth="120"Height="120"Margin="5"HorizontalAlignment="Stretch"VerticalAlignment="Stretch"Click="Button_Click"Tag="{Binding}"ToolTip="{Binding Text, Mode=OneTime}"ToolTipService.InitialShowDelay="240">
                <Button.Content>
                    <StackPanel>
                        <PathWidth="64"Height="64"Data="{Binding Value}"Fill="Green"Stretch="Uniform" />
                        <TextBlockMargin="0,10,0,10"FontSize="14"FontWeight="Normal"Foreground="Blue"Text="{Binding Text}"TextAlignment="Center" />
                    </StackPanel>
                </Button.Content>
            </Button>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.Template>
        <ControlTemplateTargetType="ItemsControl">
            <ScrollViewerWidth="Auto"CanContentScroll="True"VerticalScrollBarVisibility="Visible">
                <ItemsPresenter/>
            </ScrollViewer>
        </ControlTemplate>
    </ItemsControl.Template>
</ItemsControl>

最终我们可以获得下面的界面效果。

介绍了大致的加载和显示的界面代码,我们来看看如何增加 Geometry图标,我们前面介绍到可以结合阿里矢量图标库使用的,那么我们如何下载那些线上的图标库为我们所用呢。

阿里矢量图标库的地址:
https://www.iconfont.cn/

我们打开官网,如下界面所示。

首页会列出一些新图标,我们也可以根据关键字查询指定的图表来定位处理。

按住CTRL+ShIFT+I 键进入开发者模式,查看元素的定义,找到对应的图标位置,打开代码获得Path的内容,如下操作所示。

注意在元素代码中切换位置,同时在UI上获得具体的图标选中提示,通过提示确定Path的位置即可。

然后把这段Path的值复制到我们的 Geometries\Custom.xaml  文件中,如下所示。

上面的图表Path只有一个,有时候 阿里矢量图标库使用Geometry图标有多个Path的组合,我们如果也要采用,那么定义稍微调整一下。

通过GeometryGroup来定义父级,然后添加多个PathGeometry集合即可,如下代码所示。

通过这样的添加,我们就可以运行UI,看到加载的Geometry图标集合了。

以上通过介绍lepoco/wpfui 项目的图标库、Font-Awesome-WPF 图标库以及阿里矢量图标库的几种方式,实现了不同场景下的图表显示和处理。