把 ChatGPT 加入 Flutter 開發,會有怎樣的體驗?

聲網發表於2023-02-28

前言

ChatGPT 最近一直都處於技術圈的討論焦點。它除了可作為普通使用者的日常 AI 助手,還可以幫助開發者加速開發進度。聲網社群的一位開發者"小猿"就基於 ChatGPT 做了一場實驗。僅 40 分鐘就實現了一個互動直播 Demo。他是怎麼做的呢?他將整個過程記錄了下來。
(文章轉載自開發者的個人部落格,以下為正文)


“遇事不決,AI 力學” ~ ChatGPT 可以說是 2023 開年最熱門的話題, 它不僅在極短時間內風靡了整個技術圈,更是病毒式地席捲了圈外的各個行業,並對各大企業都起到了實質性影響:

  • 谷歌緊急推出 “Bard” 對抗 ChatGPT
  • 微軟釋出新 Bing 整合 ChatGPT
  • 復旦釋出首個類 ChatGPT 模型 MOSS
  • 國內阿里、百度、崑崙萬維、網易、京東都開始新一輪 AI 軍備

那 ChatGPT 究竟有什麼魔力能讓“群雄折腰”?這和 ChatGPT 的實現有很大關係:

與以往的統計模型不行,ChatGPT 不是那種「一切都從語料統計裡學習」的 AI,相反 ChatGPT 具備有臨場學習的能力,業內稱之為 in-context learning ,這也是為什麼 ChatGPT 可以在上下文中學習的原因。

ChatGPT 屬於 AI 領域在商用技術上的重大突破,當然,本篇我們不是要討論ChatGPT 的實現邏輯,而是 ChatGPT 會怎麼樣加速我們的開發?

PS:在此之前有人透過指示在 ChatGPT 介面下實現了一個虛擬機器,雖然這是一個極端的例子,但是可以很直觀地感受到:「ChatGPT 對我們開發的影響是肉眼可見」。

那 ChatGPT 在實際工作中是如何影響我們的開發?為了更直觀,下面我們用一個開發場景來模擬這個流程。

基於 ChatGPT 開發

01 開發之前

假設我們現在有一個開發「直播」的需求,那我們可以直接求助 ChatGPT:

「開發一個直播app,是使用第三方SDK好還是自己從0開發好」?

如下圖所示,從回答上可以看到,AI 建議我們根據團隊實際情況去選擇,而在知曉「我的團隊只有 5 個人」的情況後,它建議我選擇採用 “接入第三方 SDK” 的方式更合理。

485a4e28751194407b54a15dca4afd05.png

3c8ad01b14108039c64d3a93d227142a.png

那麼選擇 “接入第三方 SDK” ,接下來的問題就是:「選擇做直播,在中國推薦使用哪些廠家的 SDK」?

如下圖所示,這個問題 ChatGPT 同樣提供了多個選項,從選項裡看聲網、騰訊雲和阿里雲好像都符合我們要求,而在接著的「優勢問題」對比上看,這三個選項都“不相伯仲”,那我們就在再細化問題。

1b2229303f4d974701d9dcf42cae17b7.png

a5f82300513e5c2d8d146593d57ecd94.png

假設我們希望直播可以有更多“互動能力”,那麼把問題修改為 「做互動直播,更推薦使用哪一個廠家的 SDK」 ,截圖如下圖所示,這次我們得到了更明確的答覆,看來聲網的 SDK 會更貼合我們的需求。

f5a882630271d203a9f6568204087527.png

為了更放心這個選擇,我們透過 聲網 SDK 的優勢」「什麼產品使用了聲網SDK」 兩個問題進行提問,如下圖所示,從回覆上看聲網作為一個全球化的廠家,在音影片領域還是值得相信。同時,還有包括小米、陌陌等產品都使用了聲網的服務。那麼就按照 AI 的建議選擇聲網 SDK 吧。

b073f5480dd64d46e2372bdea5699c51.png

6ece2421d7116898de2df0c18e599550.png

有沒有發現,在獲取資料的檢索方式上,ChatGPT 確實比搜尋引擎更直觀且高效。

那麼敲定完 SDK ,接下來我們需要選擇應用的開發框架,我們把需求限定在 Android 和 iOS,更好是能相容 Web,覆蓋整個移動端 ,因為團隊人數不多,所以我們希望採用跨平臺開發來節約成本,那麼問題就是:

「移動端哪個跨平臺框架更適合做直播」?

如下圖所示,得到的答案有 React Native 和 Flutter ,而恰好在 Flutter 回覆裡可以看到聲網 SDK 的存在,所以我們可以敲定 App 開發框架就選 Flutter 了。

8ec422dfe303acabd93fc56cf042d65c.png

最後,在開發之前,我們還需要繼續提問 「如何獲取聲網 SDK「使用聲網 SDK 需要做什麼」,這樣我們就可以在開始開發之前提前準備好需要的東西。

441ee681f837ad90ded143e8838092ea.png

3e99eccc4946b23eb950e3bafeb82e3b.png

關於註冊獲取 App ID 等步驟這裡就省略了,畢竟目前這部分 ChatGPT 也無能為力。

02 開始開發

那麼到這裡我們就假定大家已經準備好了開發環境,接下來可以直接進行開發。

我們還是繼續面向 ChatGPT 開發,首先我們的提問是:「用聲網的 Flutter SDK agora_rtc_engine 6.1.0 寫一個視訊通話頁面,給我 dart 程式碼」 ,結果如下 GIF 所示,可以看到 ChatGPT 開始了瘋狂的輸出:

為什麼關鍵詞是「視訊通話」?因為它比直播場景更精準簡單,生成的程式碼更靠譜(經過提問測試),而基於視訊通話部分,後面我們可以快速擴充為互動直播場景;而指定版本是為了避免
AI 使用舊版本 API。

從上門的程式碼生成可以看到,ChatGPT 生產的程式碼是自帶中文註釋,更貼心的是,如下圖所示,在生成的程式碼末尾還給你解釋了這段程式碼的實現邏輯,就像一個“知心大姐姐”。

5f47e0ab223cc048e2164c02ff8d4b9d.png

從這裡也可以感覺到 ,ChatGPT 不是一個單純的完全只會基於語料答覆整合的 AI 。

當然,直接複製生成的程式碼後會發現這段程式碼會報錯,這和 ChatGPT 目前的模型資料版本有一定關係,所以針對生成的程式碼我們需要做一定手動調整,比如:

  • 採用 createAgoraRtcEngineinitialize 建立和初始化 RtcEngine
  • setEventHandler 修改為最新的 registerEventHandler
  • AgoraRenderWidget 修改為 AgoraVideoView

最後修改程式碼如下,其中 80% 以上的邏輯都來自 ChatGPT 的自動生成,雖然沒辦法做到“直出”,這無疑大大提高了開發的生產力。

class VideoCallPage extends StatefulWidget {
  final String channelName;

  const VideoCallPage({Key? key, required this.channelName}) : super(key: key);

  @override
  _VideoCallPageState createState() => _VideoCallPageState();
}

class _VideoCallPageState extends State<VideoCallPage> {
  late RtcEngine _engine;
  bool _localUserJoined = false;
  bool _remoteUserJoined = false;
  int? rUid;

  @override
  void initState() {
    super.initState();
    initAgora();
  }

  @override
  void dispose() {
    _engine.leaveChannel();
    super.dispose();
  }

  Future<void> initAgora() async {
    await [Permission.microphone, Permission.camera].request();

    _engine = createAgoraRtcEngine();
    await _engine.initialize(RtcEngineContext(
      appId: config.appId,
      channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
    ));

  

    _engine.registerEventHandler(RtcEngineEventHandler(
      onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
        setState(() {
          _localUserJoined = true;
        });
      },
      onUserJoined: (connection, remoteUid, elapsed) {
        setState(() {
          _remoteUserJoined = true;
          rUid = remoteUid;
        });
      },
      onUserOffline: (RtcConnection connection, int remoteUid,
          UserOfflineReasonType reason) {
        setState(() {
          _remoteUserJoined = false;
          rUid = null;
        });
      },
    ));

    await _engine.enableVideo();

    await _engine.startPreview();

    await _engine.joinChannel(
      token: config.token,
      channelId: widget.channelName,
      uid: config.uid,
      options: const ChannelMediaOptions(
        channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
        clientRoleType: ClientRoleType.clientRoleBroadcaster,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("VideoCallPage"),),
      body: Center(
        child: Stack(
          children: [
            _remoteUserJoined ? _remoteVideoView(rUid) : _placeholderView(),
            _localUserJoined ? _localVideoView() : _placeholderView(),
          ],
        ),
      ),
    );
  }

  Widget _placeholderView() {
    return Container(
      color: Colors.black,
    );
  }

  Widget _remoteVideoView(id) {
    return AgoraVideoView(
      controller: VideoViewController.remote(
        rtcEngine: _engine,
        canvas: VideoCanvas(uid: id),
        connection: RtcConnection(channelId: widget.channelName),
      ),
    );
  }

  Widget _localVideoView() {
    return Positioned(
      right: 16,
      bottom: 16,
      width: 100,
      height: 160,
      child: AgoraVideoView(
        controller: VideoViewController(
          rtcEngine: _engine,
          canvas: const VideoCanvas(uid: 0),
        ),
      ),
    );
  }
}

接下來,如下圖所示,在將專案執行到手機和 PC 端之後,可以看到我們就完成了最簡單的直播影片場景,而基於我們打算做直播的念頭僅僅過去了 40 分鐘,這其中還包含了註冊聲網賬號和申請 App ID 的過程,我們透過簡單的提問、複製、貼上、修改,就完成了一個直播需求的 demo。

7f5ca3395f7b925bbf71a12e3af28695.png

紅色方塊是後期加上的打碼~

那麼到這裡,雖然目前為止 demo 專案還不是互動直播,但是基於這個 demo 實現互動直播場景不會太難,因為你已經跑通了整個 SDK 的鏈路流程了。

03 進階開發

那假設我們需要繼續往互動直播的方向開發,那麼我們肯定會遇到“互動”這個需求,比如「收到使用者傳送的一段內容後畫面彈出一個動畫」 這樣的需求。

那麼首先我們要知道聲網 SDK 如何監聽使用者傳送的內容,所以接下來我們繼續提問:「如何使用聲網的 agora_rtc_engine 6.1.0 監聽別人傳送的文字訊息」 ?

這裡為什麼還強制寫 agora_rtc_engine 6.1.0 ?因為如果不寫,預設可能會輸出 4.x 版本的老 API。

儘管得到的答案並不是 Dart 程式碼而是 OC ,但是關鍵詞 registerEventHandlerMessage 我們捕抓到了,簡單對比一下,就是 Flutter SDK 裡的 registerEventHandler 物件,可以發現平替的介面就是 onStreamMessage 回撥。

f9fe23a4777fb93fd223a7df22ca82cd.png

那麼接著就是彈出什麼內容,因為我們沒有素材,假設還沒有設計師,那不如就讓 ChatGPT 幫我們畫一隻兔子吧,不過測試結果並不好,如下圖所示,從輸出結果上看 ,這並不是我們想要的。

這裡是我自己加的粉色,不然都是白色會糊成一坨,不得不說 ChatGPT 在繪製能力上“很抽象”。

7e35eda514a88394dc57481a0f613afa.png

9013334862905de2f154f73fa75841ed.png

所以 ChatGPT 有時候也不是很智慧,可能目前在繪畫理解上它還沒那麼成熟, 但是沒問題, ChatGPT 是可以透過上下文學習“調教”的,比如我們覺得兔子的耳朵形狀太離譜,那麼我們可以讓 ChatGPT 給我們調整。

如下所示,雖然調整之後依然不對,但是比起一開始是不是好很多了?

488ea73ecb14f0e0a8001f94053fa933.png

d89cb5b57861f5ed0e3ba8d3fe036e37.png

這就是 ChatGPT 在每次會話上下文裡學習的表現。

然後我們在兔子耳朵的基礎上再讓 ChatGPT 補全兔子頭,雖然最終的效果依然不理想,但是比起一開始已經進步了很多。

6a75a7fbd750ed7eeb3faa0fa9dd5c51.png

同時我們還讓 ChatGPT 給我們畫了一個“星星”,然後結合這兩個 Canvas 繪製的素材,我們在程式碼裡設定接收到 "兔子" 和 星星 文字的時候,就彈出一個放大動畫效果。

最終執行後效果如下 GIF 所示,看起來很簡陋,但是要知道,我們只是經過了簡單的複製/貼上就完成了這樣的效果,這難道不是開發效率的極大提高?

原始碼在後面。

在這裡插入圖片描述

來自 ChatGPT 的兔子頭程式碼:

class BunnyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 設定畫筆
    final paint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;

    final path = Path();

    // 繪製左耳
    path.moveTo(size.width / 2 - 40, size.height / 2 - 80);
    path.lineTo(size.width / 2 - 60, size.height / 2 - 120);
    path.quadraticBezierTo(size.width / 2 - 70, size.height / 2 - 135,
        size.width / 2 - 50, size.height / 2 - 160);
    path.lineTo(size.width / 2 - 30, size.height / 2 - 120);
    path.quadraticBezierTo(size.width / 2 - 40, size.height / 2 - 100,
        size.width / 2 - 40, size.height / 2 - 80);

    // 繪製路徑
    canvas.drawPath(path, paint);

    final path2 = Path();

    // 繪製右耳
    path2.moveTo(size.width / 2 + 40, size.height / 2 - 80);
    path2.lineTo(size.width / 2 + 60, size.height / 2 - 120);
    path2.quadraticBezierTo(size.width / 2 + 70, size.height / 2 - 135,
        size.width / 2 + 50, size.height / 2 - 160);
    path2.lineTo(size.width / 2 + 30, size.height / 2 - 120);
    path2.quadraticBezierTo(size.width / 2 + 40, size.height / 2 - 100,
        size.width / 2 + 40, size.height / 2 - 80);

    // 繪製路徑
    canvas.drawPath(path2, paint);

    final path3 = Path();

    // 繪製頭部
    final rect =
        Rect.fromLTWH(size.width / 2 - 60, size.height / 2 - 140, 120, 120);
    path3.addOval(rect);

    // 繪製路徑
    canvas.drawPath(path3, paint);

    final path4 = Path();
    // 繪製眼睛
    final leftEyeCenter = Offset(size.width / 2 - 20, size.height / 2 - 80);
    final rightEyeCenter = Offset(size.width / 2 + 20, size.height / 2 - 80);
    final eyeRadius = 8.0;

    path4.addArc(
        Rect.fromCircle(center: leftEyeCenter, radius: eyeRadius), 0, pi * 2);
    path4.addArc(
        Rect.fromCircle(center: rightEyeCenter, radius: eyeRadius), 0, pi * 2);

    // 設定畫筆
    final paint2 = Paint()
      ..color = Colors.black
      ..style = PaintingStyle.fill;

    // 繪製路徑
    canvas.drawPath(path4, paint2);

    final path5 = Path();

    // 繪製鼻子
    final noseCenter = Offset(size.width / 2, size.height / 2 - 50);
    final noseRadius = 10.0;
    path5.addArc(
        Rect.fromCircle(center: noseCenter, radius: noseRadius), 0, pi * 2);

    // 繪製路徑
    canvas.drawPath(path5, paint2);
  }
  @override
  bool shouldRepaint(BunnyPainter oldDelegate) => false;
}

來自 ChatGPT 的星星程式碼:

class StarPaint extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: HeartPainter(),
      size: Size(50, 50),
    );
  }
}

class StarPaint extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.fill;
    final path = Path();
    final halfWidth = size.width / 2;
    final halfHeight = size.height / 2;
    final radius = halfWidth;

    path.moveTo(halfWidth, halfHeight + radius);
    path.arcToPoint(
      Offset(halfWidth + radius, halfHeight),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    path.arcToPoint(
      Offset(halfWidth, halfHeight - radius),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    path.arcToPoint(
      Offset(halfWidth - radius, halfHeight),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    path.arcToPoint(
      Offset(halfWidth, halfHeight + radius),
      radius: Radius.circular(radius),
      clockwise: true,
    );
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant StarPaint oldDelegate) {
    return false;
  }
}

自己補充的監聽文字、傳送文字和動畫效果程式碼:

onStreamMessage: (RtcConnection connection, int remoteUid, int streamId,
    Uint8List data, int length, int sentTs) {
  var message = utf8.decode(data);
  if (message == "兔子") {
    showDialog(
        context: context,
        builder: (context) {
          return AnimaWidget(Rabbit());
        });
  } else if (message == "星星") {
    showDialog(
        context: context,
        builder: (context) {
          return Center(
            child: AnimaWidget(StarPaint()),
          );
        });
  }
  Future.delayed(Duration(seconds: 3), () {
    Navigator.pop(context);
  });
},

Future<void> _onPressSend() async {
  try {
    final streamId = await _engine.createDataStream(
        const DataStreamConfig(syncWithAudio: false, ordered: false));
    var txt = (Random().nextInt(10) % 2 == 0) ? "星星" : "兔子";
    final data = Uint8List.fromList(utf8.encode(txt));
    await _engine.sendStreamMessage(
        streamId: streamId, data: data, length: data.length);
  } catch (e) {
    print(e);
  }
}

class AnimaWidget extends StatefulWidget {
  final Widget child;

  const AnimaWidget(this.child);

  @override
  State<AnimaWidget> createState() => _AnimaWidgetState();
}

class _AnimaWidgetState extends State<AnimaWidget> {
  double animaScale = 1;

  @override
  void initState() {
    super.initState();
    Future.delayed(Duration(seconds: 1), () {
      animaScale = 5;
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
        scale: animaScale,
        duration: Duration(seconds: 1),
        curve: Curves.bounceIn,
        child: Container(child: widget.child));
  }
}

相信到這裡大家應該可以感受到 ChatGPT 提高開發效率的魅力,甚至你還可以把 ChatGPT 整合到你的直播場景裡,透過 Flutter 上的 chatgpt_api_client 外掛,你可以在 App 裡直接向 ChatGPT 提問,比如透過 OpenAI 的 API 實現一個可以互動的虛擬主播。

我怎麼知道這個外掛?肯定也是問 ChatGPT 的啊~

be4bc053807800e97312a8a99ce4b549.png

4175cf6f2a1c9b306e26e2a03d2c7e49.png

04 最後

到這裡,相信大家應該能感受到,在使用 ChatGPT 之後,整個開發效率能夠得到很大的提升,特別是內容檢索的高效和準確上比搜尋引擎更加靠譜,另外也能幫我們完成一些“體力活”形式的程式碼。

當然我們也看到了目前 ChatGPT 並不能完全替代人工,因為它在很多方面生成的內容並不完美,特別是很多程式碼還是需要我們人工調整,但是這並不影響 ChatGPT 的價值。

最後引用我曾經看到過的關於 ChatGPT 的一些評價:

「當你抱怨 ChatGPT鬼話連篇滿嘴跑火車的時候,這可能有點像你看到一隻猴子在沙灘上用石頭寫下1+1=3。它確實算錯了,但這不是重點。它有一天會算對的。」

我相信 AI 並不是直接取代人類的方式,因為它對社會的擠壓不是從水平上碾壓,而是劣幣驅逐良幣,比如有位大佬就說過:「乙方最討厭甲方什麼都不懂還bb,但乙方的議價權恰恰來源於甲方什麼都不懂還 bb」 ,而現在 ChatGPT 在慢慢消磨掉整個議價權。

總的來說「ChatGPT 只是一個產品,它不代表的整個技術的“上限” ,它代表的是技術已經到達商用的臨界點」。

現在,它在慢慢成為開發圈子裡的習慣,和曾經的 Copilot 一樣,而同時它在其他領域如文字編排等的能力,甚至遠超它在開發領域的價值。


歡迎開發者們也嘗試體驗聲網 SDK,實現實時音影片互動場景。現註冊聲網賬號下載 SDK,可獲得每月免費 10000 分鐘使用額度。如在開發過程中遇到疑問,可在聲網開發者社群與官方工程師交流。

相關文章