當創(chuàng)客奶爸遇見AI Agent:用行空板+Coze打造定格動畫生成器,會說話的相機讓5歲娃完成導演夢

5小時前
110
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

項目背景

大家好,我是旺仔爸爸,一名喜歡造物的創(chuàng)客奶爸,近三年未更新內(nèi)容,不知道你們有沒有忘記這個賬號。

在當今數(shù)字化時代,AI技術正以驚人的速度改變著我們的生活與創(chuàng)作方式,這次我?guī)硪粋€和AI有點關聯(lián)的小項目,和大家分享。

在孩子的成長過程中,創(chuàng)意與想象力的培養(yǎng)至關重要。我的孩子在5歲時便對制作定格動畫產(chǎn)生了濃厚的興趣,他喜歡用積木搭建場景并拍攝定格動畫視頻。然而,每次都需要父母作為攝影師和剪輯師協(xié)助完成,這不僅耗時,也限制了孩子的自主創(chuàng)作。于是,我萌生了一個想法:能否制作一個智能設備,讓孩子能夠獨立完成定格動畫的拍攝與制作呢?這個想法成為了我創(chuàng)作這個項目的初衷。

創(chuàng)客解法:用智能硬件+Agent技術打造自動化設備,實現(xiàn)兒童獨立創(chuàng)作。

通過本次項目的制作,我發(fā)現(xiàn)開源硬件和Agent是一個非常不錯的搭檔,不管是在教育領域還是創(chuàng)客領域,都可以幫助我們實現(xiàn)很多以前沒辦法實現(xiàn)的創(chuàng)意。我會持續(xù)更新關于這方面的內(nèi)容,大家可以關注一下,也歡迎有同樣興趣的同學一起交流

知識背景

本次我們的主題是定格動畫,首先我們了解一下什么是定格動畫

定格動畫,這種古老而迷人的藝術形式,起源于1907年,通過逐格拍攝對象并連續(xù)放映,創(chuàng)造出仿佛被賦予生命的奇妙畫面。無論是黏土偶、木偶還是混合材料的角色,都能在定格動畫的世界里活靈活現(xiàn)。它的制作原理簡單而直觀:拍攝多張照片,然后將這些照片連續(xù)播放,形成流暢的動畫效果。這種獨特的藝術形式,不僅能夠激發(fā)孩子們的創(chuàng)造力,還能培養(yǎng)他們的耐心與專注力。

設計思路

定格動畫簡單理解就是拍攝多張照片然后連續(xù)放映。能勝任這項工作的設備需要能看(攝像頭)、會聽說(麥克風、喇叭)、可以思考(處理數(shù)據(jù)),最好還能調(diào)用AI工具

具備這樣能力的設備有電腦,樹莓派(核桃派等),行空板M10,esp32等。

在眾多可勝任定格動畫拍攝與制作的設備中,行空板M10以其卓越的性能、豐富的功能和出色的便攜性脫穎而出。它不僅擁有堪比樹莓派的處理能力,還集成了觸摸屏、擴展接口,并預裝了多種Python第三方庫,極大地簡化了配置過程。此外,行空板M10的電源擴展版設計,使其在便攜性上更勝一籌,非常適合用于制作便攜式智能設備。

器材清單

    1.?行空板M102.?擴展板(電池+IO)3.?USB攝像頭4.?藍牙功放5.?3歐4W喇叭*26.?按鍵*3、五金件若干

方案確定后,開始設計

設計制作

人靠衣裳馬靠鞍,必須要有一個不錯的外觀結(jié)構才能拿得出手,沒準哪個奶爸媽看中潛力股就投了呢

圖紙設計

既然是拍攝照片,設計成一個相機的風格會更具象化,

整體是一個盒子結(jié)構,將行空板、喇叭固定在盒子中,攝像頭部分以一個鏡頭的造型結(jié)構來呈現(xiàn),細節(jié)部分需要注意預留type-c接口、電源開關、三個按鍵的孔位,圖紙設計如下

加工零件

圖紙設計完成后,我們使用激光切割機把圖紙加工出來,加工完成后的零件如下圖所示

零件加工完成,下面將電子部件和結(jié)構零件進行組裝

組裝

組裝分兩部分,電路部分和零件部分

電路接線

本次作品電路設計沒有其他多余的外設,攝像頭鏈接至行空板M10的USB接口,為了節(jié)省空間,這里的USB端口需要選用一種側(cè)邊接線規(guī)格的。而藍牙功放板的供電和攝像頭共用即可,音頻播放是通過藍牙與行空板連接,這里連接USB口只是為了供電,藍牙功放板和行空板之間沒有進行數(shù)據(jù)傳輸

零件組裝

下面開始零件組裝,第一步將行空板和擴展板按照官方的說明組合起來

第二步,我們找出切好的木制前面板,將行空板和前面板組合在一起,隨后安裝側(cè)面邊框,根據(jù)按鍵、type-c、電源等接口調(diào)整邊框的安裝方向,需要注意的細節(jié)是,這里我們用到了幾個白色的塑料按鈕,在安裝側(cè)邊框是需要提前將按鈕預置進去

第三步,將藍牙功放和喇叭的電路連接,藍牙功放板使用行空板的USB供電

接著安裝攝像頭,攝像頭使用滾花銅柱固定在背板上

攝像頭固定后,給它裝上鏡頭外觀結(jié)構

最后來看一下成品吧

組裝完成,最后就是程序設計了,開始程序設計前,我們需要先理清楚思路

程序設計

編程思路

下圖是本次程序設計的基本思路,使用行空板M10調(diào)用攝像頭拍攝圖像,用戶根據(jù)拍攝場景選擇喜歡的音樂主題,之后上傳文件至Coze Agent工作流,等待合成視頻后將視頻鏈接地址以二維碼的形式返回給行空板,用戶可用手機掃碼觀看視頻,也可以將視頻以二維碼的形式或鏈接的形式傳播

理解了定格動畫生成器的工作流程,下面我們來逐步拆解學習每個部分

本次作品我們使用Mind+軟件中的Python模式來為行空板M10編寫程序,軟件可在mindplus.cc下載,關于軟件的使用方法可以參考官方教程,這里不再贅述

攝像頭調(diào)用

第一步,先來學習攝像頭的使用方法,調(diào)用攝像頭需要使用Opencv庫,如未安裝,可在mind+軟件下方的終端輸入pip install opencv進行安裝,之后我們輸入如下程序進行測試

 

import?cv2
import?time
import?os

IMAGE_FOLDER =?'/root/mindplus/M10/img/'
deftake_photo():
? ??"""拍攝照片并保存到指定文件夾"""
? ? cap = cv2.VideoCapture(0)
? ? cap.set(cv2.CAP_PROP_FRAME_WIDTH,?320) ?#設置攝像頭圖像寬度
? ? cap.set(cv2.CAP_PROP_FRAME_HEIGHT,?240)?#設置攝像頭圖像高度
? ??ifnot?cap.isOpened():
? ? ? ??print("無法打開攝像頭")
? ? ? ??returnFalse
? ??# 確保目錄存在
? ? os.makedirs(IMAGE_FOLDER, exist_ok=True)
? ? cv2.namedWindow('Video Cam',cv2.WND_PROP_FULLSCREEN) ?# 構建一個窗口,名稱為Video Cam,默認屬性為可以全屏
? ? cv2.setWindowProperty('Video Cam', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
? ? i=0
? ??print("按 'a' 拍照,按 'b' 退出")
? ??while?cap.isOpened():
? ? ? ? ret,frame = cap.read()
? ? ? ??ifnot?ret:
? ? ? ? ? ??print("攝像頭讀取失敗")
? ? ? ? ? ??break
? ? ? ? cv2.imshow('Video Cam', frame)
? ? ? ? key = cv2.waitKey(1) &?0xFF
? ? ? ??if?key ==?ord('a'): ?# 按a保存
? ? ? ? ? ? timestamp =?int(time.time())
? ? ? ? ? ? path = os.path.join(IMAGE_FOLDER,?f"photo_{i}_{timestamp}.jpg") ?# 安全的路徑拼接
? ? ? ? ? ??if?cv2.imwrite(path, frame):
? ? ? ? ? ? ? ??print(f"保存成功:?{path}")
? ? ? ? ? ? ? ? i +=?1
? ? ? ? ? ??else:
? ? ? ? ? ? ? ??print(f"保存失敗:?{path}")
? ? ? ??elif?key ==?ord('b'): ?# 按b退出
? ? ? ? ? ??print("退出拍照模式")
? ? ? ? ? ??break
? ? cap.release()
? ? cv2.destroyAllWindows()
? ??return?i ??# 返回拍攝照片數(shù)量

if?__name__ ==?'__main__':
? ? take_photo()

運行之后會看到如下結(jié)果

在行空板M10的屏幕中出現(xiàn)了攝像頭的畫面,但方向似乎有些不太對,行空板默認是豎向顯示,而我們本次的作品需要把屏幕橫向顯示

這里我們需要在畫面展示之前使用下面的指令來將屏幕逆時針旋轉(zhuǎn)90°

 

frame = cv2.rotate(frame,cv2.ROTATE_90_COUNTERCLOCKWISE) #逆時針旋轉(zhuǎn)90度
cv2.imshow('Video Cam', frame)

調(diào)整之后畫面就顯示正常

接著我們解釋一下代碼中需要注意的部分。首先我們設置了圖片拍攝后的存放路徑,為了保持文件之間的條理性我們使用了絕對路徑來存放文件IMAGE_FOLDER = '/root/mindplus/M10/img/',與絕對路徑相對的相對路徑,兩種方式各有特點,絕對路徑從根目錄開始,不受當前目錄影響;而相對路徑指的是當前目錄,使用相對簡單

制作定格動畫需要保存拍攝的多張照片合成視頻,為來保證文件的唯一性,這里我們借用時間函數(shù)來命名文件

 

timestamp = int(time.time())
path = os.path.join(IMAGE_FOLDER, f"photo_{i}_{timestamp}.jpg") ?# 安全的路徑拼接
cv2.imwrite(path, frame)

為了讓用戶方便使用,我們設置用行空板板載的A、B來拍攝和保存,行空板的A、B鍵和Opencv庫中監(jiān)測電腦鍵盤輸入的指令用法相同,可以使用同樣的方法來編寫按鍵控制程序

 

key = cv2.waitKey(1) &?0xFF
if?key ==?ord('a'): ?# 按a保存
? ??print("拍照") ? ? ? ?
elif?key ==?ord('b'): ?# 按b退出
? ??print("退出拍照模式")
? ??break

運行結(jié)果如下

上述程序封裝在一個take_photo()方便調(diào)用

為了讓程序能夠重復使用,上一次拍攝的照片不要影響下一次的視頻合成,我們這里需要在調(diào)用拍照程序之前將保存照片的文件夾中的文件清除,刪除文件的指令可以用os.unlink()或者os.remove()代碼如下,調(diào)用將該函數(shù)需要插在拍照之前

 

def?delete_files(folder_path):
? ??"""清空文件夾內(nèi)所有內(nèi)容(保留文件夾本身)"""
? ??if?os.path.exists(folder_path):
? ? ? ??for?filename?in?os.listdir(folder_path):
? ? ? ? ? ? file_path = os.path.join(folder_path, filename)
? ? ? ? ? ??try:
? ? ? ? ? ? ? ??if?os.path.isfile(file_path):
? ? ? ? ? ? ? ? ? ? os.unlink(file_path)
? ? ? ? ? ? ? ? ? ??print(f"已刪除:?{file_path}")
? ? ? ? ? ??except?Exception?as?e:
? ? ? ? ? ? ? ??print(f"刪除文件失敗:?{e}")

運行結(jié)果如下

到目前,我們的作品就實現(xiàn)了能看功能,也就是具備了采集照片的能力

語音合成與播放

下面來實現(xiàn)會說的能力,本次作品的功能并不需要識別用戶說的內(nèi)容,只需要將機器合成的內(nèi)容播放出來即可。這里分兩部分來完成,第一步要實現(xiàn)語音合成,第二步再將合成的音頻文件播放出來。

先來實現(xiàn)語音合成,下面的代碼中使用了微軟的edge_tts實現(xiàn)語音識別,關于語音合成我比較了百度、訊飛、Pytts,最終我選擇了edge_tts,原因是它不需要設置API,音色比較豐富,在調(diào)用該庫時,同樣需要在終端安裝,輸入pip install edge_tts

 

def?generate_audio(text:?str) ->?None:
? ? voice =?"zh-CN-XiaoyiNeural"
? ? output_file =?"audio.mp3"
? ??"""
? ? 傳入文本、語音及輸出文件名,生成語音并保存為音頻文件
? ? :param text: 需要合成的中文文本
? ? :param voice: 使用的語音類型,如 'zh-CN-XiaoyiNeural'
? ? :param output_file: 輸出的音頻文件名
? ? """
? ??async?def?generate_audio_async() ->?None:
? ? ? ??"""異步生成語音"""
? ? ? ??print(f"使用離線語音合成:?{text}")
? ? ? ? communicate = edge_tts.Communicate(text, voice)
? ? ? ??await?communicate.save(output_file)

? ??# 異步執(zhí)行生成音頻
? ? asyncio.run(generate_audio_async())

其中voice表示語言類型,支持40多種語音,還支持調(diào)節(jié)語速、音調(diào)、和音量,感興趣的朋友可以查找資料嘗試一下,這里我們只需要選擇默認即可

使用語音合成函數(shù)可以生成一個mp3格式的音頻,拿到這個音頻文件,可以使用音頻播放指令將文件播放出來,播放音頻需要使用行空板自帶的Audio庫,指令如下

 

from unihiker import Audio
audio = Audio() #實例化音頻
audio.play(output_file)

這樣語音合成,語音播放的功能已經(jīng)實現(xiàn),這里提兩點避坑指南

1、音頻播放前需要讓行空板與藍牙功放板連接,依次輸入下面幾行指令,其中藍牙地址需要更換

 

bluetoothctl ?//啟動藍牙控制器
default-agent //設置默認的藍牙代理
power on ?//打開藍牙設備
scan on ?//掃描設備
scan off?//停止掃描
trust?28:04:81:2F:6B:DB?//信任該藍牙設備
pair?28:04:81:2F:6B:DB ?//配對
connect?28:04:81:2F:6B:DB?//連接

連接方法也可以參考這篇教程https://mc.dfrobot.com.cn/thread-320407-1-1.html#pid590262;

2、語音合成的功能只能用來生成音頻文件,如要播放需要調(diào)用音頻播放指令,經(jīng)測試發(fā)現(xiàn),用edge_tts語音合成的響應速度雖說已經(jīng)很不錯了,但還是會有一點點延時,如需提升響應效率,可提前生成要播放的內(nèi)容,在需要時直接本地調(diào)用播放可大大縮短時間

 

做到能看會說之后,現(xiàn)在需要與Coze Agent進行交互了

交互

Coze工作流搭建

有的朋友可能會問,這里為什么要使用Coze呢,既然行空板性能和樹莓派類似,行空板自己就可以合成視頻呀。沒錯,確實是這樣,這里我們使用Coze來搭建Agent其實是因為新的AI工具出來后,我們選擇的一種相對來說比較合理的方案,之所以選擇Coze是因為目前它的智能體、插件等生態(tài)資源非常豐富,幾乎在一個平臺中只需要調(diào)用不同的插件就可以實現(xiàn)功能了,不用再像之前那樣調(diào)用各種平臺,查找各種資料,配置各種接口才能實現(xiàn)。其實現(xiàn)在可以做智能體的平臺也有很多,像dify、n8n、Fastgpt等都可以,其實邏輯原理是相通的,只是Coze的資源更豐富,學習起來更容易,在起步階段先以一個簡單的上手,后續(xù)遷移其他平臺也很快

在調(diào)用coze之前,我們要先在coze平臺搭建工作流

扣子是字節(jié)系的產(chǎn)品,平臺中大量的Agent應用可以嘗試體驗,國內(nèi)地址:https://www.coze.cn/

下面我們來親自搭建一個工作流

點擊如下圖所示進入開發(fā)平臺,如需注冊需要用手機號或者字節(jié)系的賬號注冊

Coze的功能很多,依次按照如下圖所示步驟創(chuàng)建工作流。什么是工作流,為什么用工作流,工作流和Agent有什么關系?想必現(xiàn)在你有很多疑問,沒關系,這個我們先不管,保留著好奇先跟著做完,(暫且先簡單理解Agent和工作流是包含關系,作用都是為了補齊大模型的短板,輔助實現(xiàn)某些功能,關于概念的理解后面我們再解釋)

工作流創(chuàng)建完成后,可以看到一個只有開始和結(jié)束兩個節(jié)點的簡單工作流,該工作流的作用就是將輸入的內(nèi)容原封不動的再輸出。不論是什么樣的工作流,都必須要有開始和結(jié)束節(jié)點,也就是輸入和輸出

如要實現(xiàn)特定的功能,只需在中間添加節(jié)點即可,如下圖所示,我們可以在開始和結(jié)束節(jié)點中間添加一個文本處理節(jié)點,作用是將輸入的內(nèi)容進行拼接后輸出,每個中間節(jié)點都會有一個輸入和輸出,需要分別設置當前節(jié)點的輸入和輸出內(nèi)容是什么,也就是圖中的input和output,這里的名稱是默認的,可以根據(jù)需求更改,數(shù)據(jù)通過數(shù)據(jù)流來傳輸,這里設置文本處理節(jié)點接收開始節(jié)點的input內(nèi)容,將字符串拼接后的結(jié)果作為結(jié)束節(jié)點輸入的內(nèi)容,當我們輸入hello+時,結(jié)果會輸出hello+world,通過以上測試就學會了工作流的基本搭建方法

下面我搭建視頻合成的工作流,視頻合成的工作流不算復雜,但也需要清晰拆解一下實現(xiàn)邏輯,工作流的輸入、輸出和中間節(jié)點如下

下面逐步實現(xiàn),

第一步,實現(xiàn)背景音樂的生成

點擊添加節(jié)點搜索BGM關鍵詞,添加背景音樂庫,該節(jié)點的使用方法很簡單,只需要輸入音樂風格即可生成背景音樂的鏈接,關于節(jié)點的使用方法也可以點擊插件詳情了解,這里設置將開始節(jié)點的music_style作為輸入內(nèi)容傳給音樂庫節(jié)點,再將輸出的bgm_url傳給結(jié)束節(jié)點,可以看到結(jié)果如下

掌握了背景音樂生成的工作流,其他的功能其實就很簡單了

第二步,實現(xiàn)單張圖片和音頻制作視頻片段

這里需要使用一個圖片制作視頻的插件節(jié)點,這個圖片合成視頻的插件來自51aigc.cc網(wǎng)站,其中api_token需要注冊賬號后在網(wǎng)站個人中心獲取,img_url可以找網(wǎng)絡圖片臨時測試,正式調(diào)用時使用行空板上傳的圖片,mp3_url來自上一個節(jié)點的輸出,設置完成后運行可看到輸出的結(jié)果為一個視頻鏈接

第三步,多個視頻片段合成完整視頻

上一步我們使用的是一個單張圖片和音頻合成視頻的插件,現(xiàn)在我們繼續(xù)使用這個視頻合成工具箱插件中的video_merging實現(xiàn)多個視頻合成完整視頻,如下圖所示,我們添加插件后,需要輸入api_token和video_urls這兩個參數(shù),其中api_token和上一步的保持一致即可,video_urls是一個數(shù)組,用來存放每個視頻片段的url鏈接,這個鏈接正是來自上一步合成的視頻,并且每一張圖片生成視頻片段后對應一個鏈接,所以這里需要以數(shù)組的形式來接收這些鏈接

到這里,有的朋友可能會問,多個視頻片段的鏈接從哪里得到呢?其實很簡單,只需要將第二步的工作重復多次即可,這里需要用到批處理節(jié)點,通過重復執(zhí)行相同的工作,將制作的多個視頻片段的url鏈接以數(shù)組的形式輸出。想必大家已經(jīng)發(fā)現(xiàn)了問題,第一步我們生成的是一個完整的背景音樂,如果是多張圖片,那豈不是需要將音頻切開分段,再與每張圖片合成視頻。的確是這樣,所以這里我們還需要一個音頻切割的插件,負責完成這項工作,如下圖所示,我們添加音頻切割插件,只需要輸入起止時間就可以將音頻切割,并以url鏈接的形式輸出,之后再將url鏈接與圖片合成視頻即可,只不過這里我們同樣需要用到批處理的方式來重復切割音頻。

這里的關鍵是要確定每段音頻的起止時間,需要使用一個代碼節(jié)點來完成這項特定功能,使用代碼節(jié)點只需要確定輸入和輸出的內(nèi)容即可,它會自動完整,輸入的內(nèi)容是圖片數(shù)組和音頻的時長,輸出的是圖片的數(shù)量,音頻時長和分段音頻的起止時間

Coze 工作流中除了有很多成熟的插件可以使用外,還提供了代碼節(jié)點,可以方便我們實現(xiàn)特殊功能,下面是本次計算音頻時長的節(jié)點代碼

 

async?defmain(args: Args) -> Output:
? ??# 從輸入?yún)?shù)中獲取音頻時長(微秒)和圖片列表
? ? params = args.params
? ? audio_duration_us = params['audio_duration']
? ? img_list = params['img_list']
? ??
? ??# 計算圖片數(shù)組的長度
? ? img_count =?len(img_list)
? ? audio_start_stopTime = []
? ??# 計算每個時間段的長度(微秒)
? ??if?img_count >?0:
? ? ? ??# 計算總時長(秒)和每段時長(秒)
? ? ? ? total_duration_sec = audio_duration_us /?1_000_000
? ? ? ? segment_duration_sec =?min(3.0, total_duration_sec / img_count)
? ? ? ??
? ? ? ??# 轉(zhuǎn)換為微秒(整數(shù)運算避免浮點誤差)
? ? ? ? segment_duration_us =?int(segment_duration_sec *?1_000_000)
? ? ? ??
? ? ? ??# 生成音頻起止時間數(shù)組
? ? ? ??for?i?inrange(img_count):
? ? ? ? ? ? start_time_us = i * segment_duration_us
? ? ? ? ? ? stop_time_us = start_time_us + segment_duration_us
? ? ? ? ? ??
? ? ? ? ? ??# 處理最后一個時間段,確??倳r長準確
? ? ? ? ? ??if?i == img_count -?1:
? ? ? ? ? ? ? ? stop_time_us =?min(stop_time_us, audio_duration_us)
? ? ? ? ? ??
? ? ? ? ? ??# 將微秒轉(zhuǎn)換為秒
? ? ? ? ? ? audio_start_stopTime.append({
? ? ? ? ? ? ? ??"start_time": start_time_us //?1000000,
? ? ? ? ? ? ? ??"stop_time": stop_time_us //?1000000
? ? ? ? ? ? })
? ??
? ??# 構建并返回輸出JSON(字典格式)
? ? ret: Output = {
? ? ? ??"img_array_length": img_count,
? ? ? ??"audio_duration": audio_duration_us,
? ? ? ??"audio_start_stopTime": audio_start_stopTime
? ? }
? ??
? ??return?ret

經(jīng)過以上三步,已經(jīng)可以生成一個定格動畫視頻了,不過現(xiàn)在還需要將視頻的URL鏈接轉(zhuǎn)換成二維碼圖片,再將圖片轉(zhuǎn)化成base64編碼,輸出給行空板,前面的內(nèi)容掌握后這幾步會非常簡單,按照下圖所示操作即可。設置完成后,試運行沒問題可以點擊發(fā)布,

發(fā)布后該工作流支持在Coze Agent中調(diào)用,也支持外部終端設備通過API接口來調(diào)用

Coze 工作流中的插件有官方插件、第三方插件以及個人用戶插件,關于插件的選擇,首選官方插件,其次是比較有實力保障的公司提供的插件,再次才是個人用戶的插件,插件根據(jù)功能的不同,分收費和免費,官方的插件免費贈送的節(jié)點用來測試基本夠用了,第三方的沒有大批量商用的花使用成本也非常低,本次我們使用的基本上是官方插件和51aigc團隊的插件,穩(wěn)定性是有保障的。關于Coze工作流搭建的環(huán)節(jié)我們已經(jīng)介紹完了,Coze是一個非常不錯的學習AI Agent的平臺,本次我們只是簡單介紹了一下與本次項目相關的功能,這只是冰山一角,后面我會再通過不同的項目分享更多關于Coze平臺的使用方法

行空板調(diào)用工作流

Coze的工作流搭建完整,現(xiàn)在我們可以調(diào)用工作流的API接口進行測試,調(diào)用工作流的方法可以參考如下官方的鏈接

 

https://www.coze.cn/open/docs/developer_guides/upload_files ? //上傳文件
https://www.coze.cn/open/docs/developer_guides/workflow_run ? //執(zhí)行工作流

本次項目,與Coze工作流有兩次交互的過程,第一次先將圖片上傳至Coze,返回圖片的ID;第二次將圖片的ID和背景音樂的主題作為輸入?yún)?shù)傳入前面搭建好的工作流中,返回包含有視頻url的二維碼

在開始之前,我們需要做兩項準備工作

1、獲取Coze的個人令牌,也就是Token,在如下地址獲取即可,設置權限時可以選擇全部,注意這個token需要及時復制保存,不支持二次查看,如果忘記需要重新生成,之前的將會失效

https://www.coze.cn/open/oauth/pats

2、獲取工作流的ID,進入到工作流后,可以在網(wǎng)址欄中看到工作流的ID,記錄下來,方便調(diào)用

準備工作做好后,我們回到mind+軟件輸入如下代碼

 

import?requests, json,base64,
COZE_TOKEN =?'pat_4BuSeWUu9JqEiH2BeYdleXbcKsyLNB4asOkAfDRRYhzAmy9JJ57dtwwkvtg8w5e4'
WORKFLOW_ID =?"7504802266752565285"
classCozeAPI:
? ??"""Coze API 操作類"""
? ??def__init__(self, access_token):
? ? ? ??self.base_url =?"https://api.coze.cn/v1"
? ? ? ??self.access_token = access_token
? ? ? ??self.headers = {
? ? ? ? ? ??"Authorization":?f"Bearer?{access_token}"
? ? ? ? }
? ??def_upload_file(self, file_path):
? ? ? ??"""上傳單個文件到Coze"""
? ? ? ? url =?f"{self.base_url}/files/upload"
? ? ? ? filename = os.path.basename(file_path)
? ? ? ??try:
? ? ? ? ? ??# 檢查文件大小
? ? ? ? ? ? file_size = os.path.getsize(file_path)
? ? ? ? ? ??if?file_size >?512?*?1024?*?1024: ?# 512MB
? ? ? ? ? ? ? ??print(f"跳過?{filename}?(超過512MB限制)")
? ? ? ? ? ? ? ??returnNone
? ? ? ? ? ??# 上傳文件
? ? ? ? ? ??withopen(file_path,?'rb')?as?file_obj:
? ? ? ? ? ? ? ? files = {'file': (filename, file_obj)}
? ? ? ? ? ? ? ? response = requests.post(url, headers=self.headers, files=files)
? ? ? ? ? ? ? ? response.raise_for_status()
? ? ? ? ? ? ? ??# 檢查響應
? ? ? ? ? ? ? ? result = response.json()
? ? ? ? ? ? ? ??if?result.get('code') ==?0:
? ? ? ? ? ? ? ? ? ??print(f"上傳成功:?{filename}")
? ? ? ? ? ? ? ? ? ??return?result['data']
? ? ? ? ? ? ? ??else:
? ? ? ? ? ? ? ? ? ??print(f"上傳失敗:?{result.get('msg',?'未知錯誤')}")
? ? ? ? ? ? ? ? ? ??returnNone
? ? ? ??except?Exception?as?e:
? ? ? ? ? ??print(f"上傳?{filename}?時出錯:?{str(e)}")
? ? ? ? ? ??returnNone
? ??defupload_images(self, folder_path):
? ? ? ??"""批量上傳圖片文件到Coze"""
? ? ? ? image_data = []
? ? ? ? valid_extensions = ('.png',?'.jpg',?'.jpeg',?'.gif',?'.bmp',?'.webp')
? ? ? ??ifnot?os.path.exists(folder_path):
? ? ? ? ? ??raise?FileNotFoundError(f"文件夾不存在:?{folder_path}")
? ? ? ??# 獲取所有圖片文件
? ? ? ? image_files = [f?for?f?in?os.listdir(folder_path)?
? ? ? ? ? ? ? ? ? ? ??if?f.lower().endswith(valid_extensions)]
? ? ? ??
? ? ? ??ifnot?image_files:
? ? ? ? ? ??print("文件夾中沒有圖片文件")
? ? ? ? ? ??return?image_data
? ? ? ??print(f"發(fā)現(xiàn)?{len(image_files)}?張圖片準備上傳")
? ? ? ??# 批量上傳
? ? ? ??for?filename?in?image_files:
? ? ? ? ? ? file_path = os.path.join(folder_path, filename)
? ? ? ? ? ? result =?self._upload_file(file_path)
? ? ? ? ? ??if?result:
? ? ? ? ? ? ? ? image_data.append(result)
? ? ? ??return?image_data
? ??defrun_workflow(self, workflow_id, parameters):
? ? ? ??"""執(zhí)行工作流"""
? ? ? ? url =?f"{self.base_url}/workflow/run"
? ? ? ? data = {
? ? ? ? ? ??"workflow_id": workflow_id,
? ? ? ? ? ??"parameters": parameters
? ? ? ? }
? ? ? ??print("執(zhí)行工作流參數(shù):")
? ? ? ??print(json.dumps(data, indent=4, ensure_ascii=False))
? ? ? ??try:
? ? ? ? ? ? response = requests.post(
? ? ? ? ? ? ? ? url,
? ? ? ? ? ? ? ? headers=self.headers,
? ? ? ? ? ? ? ? data=json.dumps(data),
? ? ? ? ? ? ? ? timeout=60# 增加超時時間
? ? ? ? ? ? )
? ? ? ? ? ? response.raise_for_status()
? ? ? ? ? ? response_data = response.json()
? ? ? ? ? ??# 檢查響應狀態(tài)碼
? ? ? ? ? ??if?response_data.get('code') !=?0:
? ? ? ? ? ? ? ??print(f"工作流執(zhí)行失敗:?{response_data.get('msg')}")
? ? ? ? ? ? ? ??returnNone? ? ?
? ? ? ? ? ??print("工作流執(zhí)行結(jié)果:")
? ? ? ? ? ??print(json.dumps(response_data, indent=4, ensure_ascii=False))
? ? ? ? ? ??return?response_data
? ? ? ??except?requests.exceptions.RequestException?as?e:
? ? ? ? ? ??print(f"工作流請求失敗:?{str(e)}")
? ? ? ? ? ??return?None

代碼中我們將與Coze交互的部分做了一個類函數(shù),其中access_token為前面獲取到的個人令牌,我們將文件上傳分成了單個文件和批量文件上傳兩種,方便函數(shù)復用,運行程序,可以拿到如下測試結(jié)果,“code”為0表示測試成功,如返回的結(jié)果不是0,則需要根據(jù)“msg”信息調(diào)整。其中“data”數(shù)據(jù)既圖片上傳后的信息,“id”的內(nèi)容在執(zhí)行工作流時使用。

 

{"code":0,"data":{"bytes":26451,"created_at":1750525076,"file_name":"online-shopping.png","id":"7518444294115147812"},"detail":{"logid":"202506220057559170147E27FFD06CBB7F"},"msg":""}

文件上傳支持多種常見的格式,單個文件大小不超過512M即可,我們在代碼中做了文件大小檢測,本次我們只需要上傳圖片即可,批量上傳后獲取到圖片ID可用來調(diào)用工作流生成視頻,調(diào)用執(zhí)行工作流函數(shù)run_workflow

時,除了傳入多張圖片ID的列表外,還需要將背景音樂主題以字符串的格式傳入。之后再將返回的二維碼顯示,這里我們封裝一個函數(shù)upload(BGM)來完成工作流的調(diào)用工作,代碼如下

 

def?upload(BGM):
? ??"""上傳圖片并執(zhí)行工作流生成視頻二維碼"""
? ??print("開始上傳和處理流程")
? ??# 初始化API
? ? api = CozeAPI(COZE_TOKEN)
? ??try:
? ? ? ??# 批量上傳照片
? ? ? ??print("n===== 上傳照片 =====")
? ? ? ? upload_results = api.upload_images(IMAGE_FOLDER)
? ? ? ??ifnot?upload_results:
? ? ? ? ? ??print("沒有成功上傳的照片,終止處理")
? ? ? ? ? ??returnFalse
? ? ? ??# 準備圖片輸入
? ? ? ? image_entries = [{"file_id": item['id']}?for?item?in?upload_results]
? ? ? ??print(f"n準備處理的圖片:?{len(image_entries)}張")
? ? ? ??# 執(zhí)行工作流
? ? ? ??print("n===== 執(zhí)行工作流 =====")
? ? ? ? workflow_result = api.run_workflow(
? ? ? ? ? ? workflow_id=WORKFLOW_ID,
? ? ? ? ? ? parameters={
? ? ? ? ? ? ? ??"img_input": image_entries,
? ? ? ? ? ? ? ??"text_input": BGM
? ? ? ? ? ? }
? ? ? ? )
? ? ? ??ifnot?workflow_result:
? ? ? ? ? ??print("工作流執(zhí)行失敗")
? ? ? ? ? ??returnFalse
? ? ? ??# 處理工作流結(jié)果
? ? ? ??if'data'in?workflow_result:
? ? ? ? ? ? workflow_output = json.loads(workflow_result['data'])
? ? ? ? ? ??if'data'in?workflow_output:
? ? ? ? ? ? ? ??# 解碼二維碼圖片
? ? ? ? ? ? ? ? bytes_data = base64.b64decode(workflow_output['data'])
? ? ? ? ? ? ? ??# 創(chuàng)建臨時文件路徑
? ? ? ? ? ? ? ? qr_path = os.path.join(IMAGE_FOLDER,?f"qrcode_{int(time.time())}.png")
? ? ? ? ? ? ? ??# 保存二維碼圖片
? ? ? ? ? ? ? ??withopen(qr_path,?"wb")?as?img_file:
? ? ? ? ? ? ? ? ? ? img_file.write(bytes_data)
? ? ? ? ? ? ? ??print(f"二維碼已保存至:?{qr_path}")
? ? ? ? ? ? ? ??# 旋轉(zhuǎn)并顯示二維碼
? ? ? ? ? ? ? ? img = Image.open(qr_path)
? ? ? ? ? ? ? ? img = img.transpose(Image.ROTATE_270)
? ? ? ? ? ? ? ??returnTrue? ? ?
? ? ? ??print("未獲取到有效的二維碼數(shù)據(jù)")
? ? ? ??returnFalse? ? ?
? ??except?Exception?as?e:
? ? ? ??print(f"程序異常:?{str(e)}")
? ? ? ??returnFalse
? ??finally:
? ? ? ??print("n上傳處理流程完成")
主頁面狀態(tài)設置

其實,到目前為止,我們已經(jīng)可以實現(xiàn)制作定格動畫視頻的功能了,只不過為了讓設備更好用,可以繼續(xù)優(yōu)化,設計一個UI交互界面,將拍攝照片、選擇音樂主題、上傳文件分別設置成不同的模塊供用戶調(diào)用。設置好的界面如下圖所示

要完成界面設計,需要提前準備一些圖標,將文件命名好后,以絕對路徑或相對路徑的形式調(diào)用

代碼如下

 

from?unihiker?import?GUI ??#導入包
from?pinpong.board?import?*
from?pinpong.extension.unihiker?import?*
gui=GUI() ?#實例化GUI類
deffirst_page():
? ??# 頁面初始化代碼...
? ??global?camera_image, Caption_text
? ??# 統(tǒng)一旋轉(zhuǎn)函數(shù)
? ??defload_rotated_image(path):
? ? ? ? img = Image.open(path)
? ? ? ??return?img.transpose(Image.ROTATE_270)
? ??# 加載并旋轉(zhuǎn)所有圖標
? ? icons = {
? ? ? ??'camera_icon': load_rotated_image("camera_icon.png"),
? ? ? ??'camera': load_rotated_image("camera.png"),
? ? ? ??'music': load_rotated_image("online-shopping.png"),
? ? ? ??'upload': load_rotated_image("upload-file.png"),
? ? ? ??'title': load_rotated_image("video-editing.png"),
? ? ? ??'chat': load_rotated_image("chat.png")
? ? }
? ? title_text = gui.draw_text(x=240, y=170, text='Stop Motion',origin='top'? ? ? ? ? ,color='black', angle=270)
? ? Caption_text = gui.draw_text(x=190, y=130, text="點擊拍照 A鍵拍攝 B鍵返回",font_size=10,origin='left', angle=270)
? ? camera_image = gui.draw_image(x=180, y=140, w=160, h=200, ?image=icons['camera'], origin='top_right', onclick=lambda: iconclick('1') )
? ? camera_icon = gui.draw_image(x=200, y=20, w=40, h=50, ?image=icons['camera_icon'], origin='top_right', onclick=lambda: iconclick('1'))
? ? music_icon = gui.draw_image(x=135, y=20, w=40, h=50, ?image=icons['music'], origin='top_right', onclick=lambda: iconclick('2'))
? ? upload_icon = gui.draw_image(x=70, y=20, w=40, h=50, ?image=icons['upload'], origin='top_right', onclick=lambda: iconclick('3'))
? ? title_icon = gui.draw_image(x=215, y=85, w=20, h=20, ?image=icons['title'], origin='top_left')
? ? chat_icon = gui.draw_image(x=180, y=100, w=20, h=20, ?image=icons['chat'], origin='top_left')
? ??# 設置全局引用
? ? app_state.caption_text = Caption_text
? ? app_state.camera_image = camera_image

界面設計的代碼其實非常簡單,只用到了文字 draw_text和圖片 draw_image控件??丶ο竺?config(需要更新的參數(shù)名=值)用來更新控件,GUI對象.remove(控件對象名)用來刪除某個控件,GUI對象.clear()用來刪除所有控件。更多用法在行空板的官方文檔有很詳細的介紹,需要的朋友可以參考,這里不再展開介紹https://www.unihiker.com.cn/wiki/m10/unihiker_python_lib_2#|-%205.3-%E5%9B%BE%E7%89%87%20draw_image

值得注意的細節(jié)是,行空的屏幕默認是豎向顯示,屏幕的分辨率是240*320,而我們本次的作品需要將行空板橫向顯示,所以不論是圖片還是文字都需要旋轉(zhuǎn)270°。而對應的坐標需要相應調(diào)整過來,關于行空板的坐標方向,以及不同控件的坐標基準點可以參考下圖

想必你已經(jīng)發(fā)現(xiàn)了,界面中的圖標設置了回調(diào)函數(shù),也就是說圖片是可以點擊的,通過函數(shù)iconclick(data)實現(xiàn)點擊不同的圖片切換不同的模式。同時在界面中的文字顯示部分由于選擇的模式不同,顯示的內(nèi)容也會相應的切換,為此我們設置了狀態(tài)管理類,并在界面初始化后,將其設置為全局引用,這樣在不同的函數(shù)中都可以方便的調(diào)用更新內(nèi)容

 

# ============== 狀態(tài)管理類 ==============
classAppState:
? ??def__init__(self):
? ? ? ??self.state =?""
? ? ? ??self.caption_text =?None
? ? ? ??self.camera_image =?None
? ? ? ??self.photo_count =?0
? ? ? ??self.bgm =?""
# 創(chuàng)建應用狀態(tài)實例
app_state = AppState()
deficonclick(data):
? ??"""處理圖標點擊事件"""
? ??# 角色功能映射
? ? ROLE_FUNCTION_MAP = {"1":?"take_photo",?"2":?"select_music",?"3":?"upload"}
? ? app_state.state = ROLE_FUNCTION_MAP.get(data,?"")
? ??print(f"狀態(tài)更新為:?{app_state.state}")
deffirst_page():
? ??# 頁面初始化代碼...
? ? ......
? ??# 設置全局引用
? ? app_state.caption_text = Caption_text
? ? app_state.camera_image = camera_image

至此,界面就設置好了,但并沒有將要執(zhí)行的函數(shù)與模式狀態(tài)綁定起來。拍攝照片,工作流調(diào)用的函數(shù)在前面我們都已經(jīng)設置完成,我們還需要設置一個選擇背景音樂主題的函數(shù),代碼如下

 

from?unihiker?import?GUI ??#導入包
from?pinpong.board?import?*
from?pinpong.extension.unihiker?import?*
Board().begin()?#初始化
defmusic():
? ??"""音樂選擇界面"""
? ??print("進入音樂選擇模式")
? ??# 隱藏相機圖標
? ? app_state.camera_image.config(x=3000)
? ??# 音樂選項配置
? ? music_options = [
? ? ? ? {"text":?"舒緩",?"x":?140,?"y":?130},
? ? ? ? {"text":?"田園",?"x":?140,?"y":?200},
? ? ? ? {"text":?"歡快",?"x":?140,?"y":?270},
? ? ? ? {"text":?"勁爆",?"x":?60,?"y":?130},
? ? ? ? {"text":?"喜慶",?"x":?60,?"y":?200},
? ? ? ? {"text":?"自然",?"x":?60,?"y":?270}
? ? ]
? ??# 創(chuàng)建音樂選項文本
? ? music_texts = []
? ??for?option?in?music_options:
? ? ? ? text = gui.draw_text(
? ? ? ? ? ? x=option["x"], y=option["y"],
? ? ? ? ? ? text=option["text"],
? ? ? ? ? ? origin='center',
? ? ? ? ? ? font_size=14,
? ? ? ? ? ? angle=270
? ? ? ? )
? ? ? ? music_texts.append(text)
? ??# 創(chuàng)建選擇框
? ? rect_x =?125
? ? rect_y =?105
? ? music_rect = gui.draw_rect(
? ? ? ? x=rect_x, y=rect_y,
? ? ? ? w=30, h=50,
? ? ? ? width=3, color=(255,?0,?0)
? ? )
? ??# 當前選擇的音樂索引
? ? current_index =?0
? ? bgm_mapping = ["舒緩",?"田園",?"歡快",?"勁爆",?"喜慶",?"自然"]
? ??# 音樂選擇循環(huán)
? ??while?app_state.state ==?"select_music":
? ? ? ??# 處理A鍵按下 - 選擇下一個音樂
? ? ? ??if?button_a.is_pressed():
? ? ? ? ? ? time.sleep(0.2) ?# 防抖延時
? ? ? ? ? ??# 更新選擇索引
? ? ? ? ? ? current_index = (current_index +?1) %?len(bgm_mapping)
? ? ? ? ? ??# 計算新位置
? ? ? ? ? ??if?current_index <?3:
? ? ? ? ? ? ? ? rect_x =?125
? ? ? ? ? ??else:
? ? ? ? ? ? ? ? rect_x =?45? ??
? ? ? ? ? ? rect_y =?105?+ (current_index %?3) *?70
? ? ? ? ? ??# 更新選擇框位置
? ? ? ? ? ? music_rect.config(x=rect_x, y=rect_y)
? ? ? ? ? ??print(f"選擇音樂:?{bgm_mapping[current_index]}")
? ? ? ??# 處理B鍵按下 - 確認選擇
? ? ? ??if?button_b.is_pressed():
? ? ? ? ? ? selected_bgm = bgm_mapping[current_index]
? ? ? ? ? ??print(f"確認選擇:?{selected_bgm}")
? ? ? ? ? ??return?selected_bgm
? ??# 如果狀態(tài)改變但未選擇,返回默認值
? ??return?bgm_mapping[0]

代碼中,我們設置了6種不同的音樂主題,為了讓用戶更直觀的選擇,設置了一個矩形選擇框,通過按下按鍵A來切換,按下B鍵確認并返回,效果如下

選擇音樂主題的函數(shù)設置完成后,我們還需要設置一個狀態(tài)選擇函數(shù),設置當點擊不同的圖標后切換到對應的函數(shù),按下B鍵退出程序,代碼如下

 

import?requests, cv2,time, json,base64, asyncio, edge_tts
from?unihiker?import?GUI ??#導入包
from?pinpong.board?import?*
from?pinpong.extension.unihiker?import?*
from?unihiker?import?Audio
from?PIL?import?Image

defhandle_take_photo():
? ??"""處理拍照操作"""
? ??# 拍照并更新照片計數(shù)
? ? app_state.photo_count = take_pictures()
? ? app_state.state =?""
? ? gui.clear()
? ? first_page()
? ? app_state.caption_text.config(text=f"已拍攝{app_state.photo_count}張照片,點擊選擇音樂")
? ? audio.play("photoOk.mp3")
defhandle_select_music():
? ??"""處理音樂選擇操作"""
? ??# 提示用戶選擇音樂
? ? app_state.caption_text.config(text="A鍵選擇 B鍵返回")
? ? audio.play("photo_music.mp3")
? ??# 選擇音樂
? ? app_state.bgm = music()
? ??print(f"選擇的音樂:?{app_state.bgm}")
? ??# 更新UI
? ? app_state.state =?""
? ? gui.clear()
? ? first_page()
? ??# 顯示狀態(tài)信息并播放語音提示
? ? app_state.caption_text.config(text=f"拍攝{app_state.photo_count}張照片 選擇主題{app_state.bgm}")
? ? generate_audio(f"拍攝{app_state.photo_count}張照片 選擇的音樂主題是{app_state.bgm},點擊文件圖標可上傳文件合成視頻")

defhandle_upload():
? ??"""處理上傳操作"""
? ? upload(app_state.bgm)
? ? app_state.state =?""
# ============== 主循環(huán)函數(shù) ==============
defselect_mode():
? ??"""主狀態(tài)循環(huán)"""
? ??# 設置回調(diào)函數(shù)映射
? ? state_handlers = {
? ? ? ??"take_photo": handle_take_photo,
? ? ? ??"select_music": handle_select_music,
? ? ? ??"upload": handle_upload
? ? }
? ??# 主循環(huán)
? ??while?button_b.is_pressed() !=?1:
? ? ? ??if?app_state.state?in?state_handlers:
? ? ? ? ? ??# 執(zhí)行對應的狀態(tài)處理函數(shù)
? ? ? ? ? ? handler = state_handlers[app_state.state]
? ? ? ? ? ??try:
? ? ? ? ? ? ? ? handler()
? ? ? ? ? ??except?Exception?as?e:
? ? ? ? ? ? ? ??print(f"狀態(tài)處理錯誤:?{str(e)}")
? ? ? ? ? ? ? ? app_state.caption_text.config(text="操作出錯,請重試")
? ? ? ? ? ? ? ? app_state.state =?""

完整代碼

最后是本次定格動畫生成器的完整代碼

 

import?requests, cv2,time, json,base64, asyncio, edge_tts
from?unihiker?import?GUI ??#導入包
from?pinpong.board?import?*
from?pinpong.extension.unihiker?import?*
from?unihiker?import?Audio
from?PIL?import?Image

# ============== 配置區(qū)域 ==============
# 修改為你的實際路徑
#IMAGE_FOLDER = r'F:MyOfficeBaiduSyncdiskdesign_filemyPythonM10img'
IMAGE_FOLDER =?'/root/mindplus/M10/img/'
COZE_TOKEN =?'pat_4BuSeWUu9JqEiH2BeYdleXbcKsyLNB4asOkAfDRRYhzAmy9JJ57dtwwkvtg8w5e4'
WORKFLOW_ID =?"7504802266752565285"
# ============== 硬件初始化 ==============
gui=GUI() ?#實例化GUI類
Board().begin()?#初始化
audio = Audio()?#實例化音頻
# ============== 狀態(tài)管理類 ==============
classAppState:
? ??def__init__(self):
? ? ? ??self.state =?""
? ? ? ??self.caption_text =?None
? ? ? ??self.camera_image =?None
? ? ? ??self.photo_count =?0
? ? ? ??self.bgm =?""
# 創(chuàng)建應用狀態(tài)實例
app_state = AppState()

deffirst_page():
? ??# 頁面初始化代碼...
? ??global?camera_image, Caption_text
? ??# 統(tǒng)一旋轉(zhuǎn)函數(shù)
? ??defload_rotated_image(path):
? ? ? ? img = Image.open(path)
? ? ? ??return?img.transpose(Image.ROTATE_270)
? ??# 加載并旋轉(zhuǎn)所有圖標
? ? icons = {
? ? ? ??'camera_icon': load_rotated_image("camera_icon.png"),
? ? ? ??'camera': load_rotated_image("camera.png"),
? ? ? ??'music': load_rotated_image("online-shopping.png"),
? ? ? ??'upload': load_rotated_image("upload-file.png"),
? ? ? ??'title': load_rotated_image("video-editing.png"),
? ? ? ??'chat': load_rotated_image("chat.png")
? ? }
? ? title_text = gui.draw_text(x=240, y=170, text='Stop Motion',origin='top'?,color='black', angle=270)
? ? Caption_text = gui.draw_text(x=190, y=130, text="點擊拍照 A鍵拍攝 B鍵返回",font_size=10,origin='left', angle=270)
? ? camera_image = gui.draw_image(x=180, y=140, w=160, h=200, ?image=icons['camera'], origin='top_right', onclick=lambda: iconclick('1') )
? ? camera_icon = gui.draw_image(x=200, y=20, w=40, h=50, ?image=icons['camera_icon'], origin='top_right', onclick=lambda: iconclick('1'))
? ? music_icon = gui.draw_image(x=135, y=20, w=40, h=50, ?image=icons['music'], origin='top_right', onclick=lambda: iconclick('2'))
? ? upload_icon = gui.draw_image(x=70, y=20, w=40, h=50, ?image=icons['upload'], origin='top_right', onclick=lambda: iconclick('3'))
? ? title_icon = gui.draw_image(x=215, y=85, w=20, h=20, ?image=icons['title'], origin='top_left')
? ? chat_icon = gui.draw_image(x=180, y=100, w=20, h=20, ?image=icons['chat'], origin='top_left')
? ??# 設置全局引用
? ? app_state.caption_text = Caption_text
? ? app_state.camera_image = camera_image

deficonclick(data):
? ??"""處理圖標點擊事件"""
? ??# 角色功能映射
? ? ROLE_FUNCTION_MAP = {"1":?"take_photo",?"2":?"select_music",?"3":?"upload"}
? ? app_state.state = ROLE_FUNCTION_MAP.get(data,?"")
? ??print(f"狀態(tài)更新為:?{app_state.state}")

defgenerate_audio(text:?str) ->?None:
? ? voice =?"zh-CN-XiaoyiNeural"
? ? output_file =?"audio.mp3"
? ??"""
? ? 傳入文本、語音及輸出文件名,生成語音并保存為音頻文件
? ? :param text: 需要合成的中文文本
? ? :param voice: 使用的語音類型,如 'zh-CN-XiaoyiNeural'
? ? :param output_file: 輸出的音頻文件名
? ? """
? ??asyncdefgenerate_audio_async() ->?None:
? ? ? ??"""異步生成語音"""
? ? ? ??print(f"使用離線語音合成:?{text}")
? ? ? ? communicate = edge_tts.Communicate(text, voice)
? ? ? ??await?communicate.save(output_file)

? ??# 異步執(zhí)行生成音頻
? ? asyncio.run(generate_audio_async())
? ? audio.play(output_file)

defdelete_files(folder_path):
? ??"""清空文件夾內(nèi)所有內(nèi)容(保留文件夾本身)"""
? ??if?os.path.exists(folder_path):
? ? ? ??for?filename?in?os.listdir(folder_path):
? ? ? ? ? ? file_path = os.path.join(folder_path, filename)
? ? ? ? ? ??try:
? ? ? ? ? ? ? ??if?os.path.isfile(file_path):
? ? ? ? ? ? ? ? ? ? os.unlink(file_path)
? ? ? ? ? ? ? ? ? ??print(f"已刪除:?{file_path}")
? ? ? ? ? ??except?Exception?as?e:
? ? ? ? ? ? ? ??print(f"刪除文件失敗:?{e}")
deftake_photo():
? ??"""拍攝照片并保存到指定文件夾"""
? ? cap = cv2.VideoCapture(0)
? ? cap.set(cv2.CAP_PROP_FRAME_WIDTH,?320) ?#設置攝像頭圖像寬度
? ? cap.set(cv2.CAP_PROP_FRAME_HEIGHT,?240)?#設置攝像頭圖像高度
? ??ifnot?cap.isOpened():
? ? ? ??print("無法打開攝像頭")
? ? ? ??returnFalse

? ??# 確保目錄存在
? ? os.makedirs(IMAGE_FOLDER, exist_ok=True)
? ? delete_files(IMAGE_FOLDER)
? ??#cv2.namedWindow('Video Cam', cv2.WINDOW_NORMAL) #創(chuàng)建窗口"Video Cam"
? ? cv2.namedWindow('Video Cam',cv2.WND_PROP_FULLSCREEN) ?# 構建一個窗口,名稱為Video Cam,默認屬性為可以全屏
? ? cv2.setWindowProperty('Video Cam', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
? ? i=0
? ??print("按 'a' 拍照,按 'b' 退出")
? ??while?cap.isOpened():
? ? ? ? ret,frame = cap.read()
? ? ? ??ifnot?ret:
? ? ? ? ? ??print("攝像頭讀取失敗")
? ? ? ? ? ??break
? ? ? ? frame = cv2.rotate(frame,cv2.ROTATE_90_COUNTERCLOCKWISE)?#逆時針旋轉(zhuǎn)90度
? ? ? ? cv2.imshow('Video Cam', frame)
? ? ? ? key = cv2.waitKey(1) &?0xFF
? ? ? ??if?key ==?ord('a'): ?# 按a保存
? ? ? ? ? ? audio.play("camera.wav")
? ? ? ? ? ? timestamp =?int(time.time())
? ? ? ? ? ? path = os.path.join(IMAGE_FOLDER,?f"photo_{i}_{timestamp}.jpg") ?# 安全的路徑拼接
? ? ? ? ? ??if?cv2.imwrite(path, frame):
? ? ? ? ? ? ? ??print(f"保存成功:?{path}")
? ? ? ? ? ? ? ? i +=?1
? ? ? ? ? ??else:
? ? ? ? ? ? ? ??print(f"保存失敗:?{path}")
? ? ? ??elif?key ==?ord('b'): ?# 按b退出
? ? ? ? ? ??print("退出拍照模式")
? ? ? ? ? ??break
? ? cap.release()
? ? cv2.destroyAllWindows()
? ??return?i ??# 返回是否拍攝了照片
deftake_pictures():
? ??# 1. 拍攝照片
? ??print("===== 拍照模式 =====")
? ? i = take_photo()
? ??if?i==0:
? ? ? ??print("未拍攝照片,程序終止")
? ??return?i
defmusic():
? ??"""音樂選擇界面"""
? ??print("進入音樂選擇模式")
? ??# 隱藏相機圖標
? ? app_state.camera_image.config(x=3000)
? ??# 音樂選項配置
? ? music_options = [
? ? ? ? {"text":?"舒緩",?"x":?140,?"y":?130},
? ? ? ? {"text":?"田園",?"x":?140,?"y":?200},
? ? ? ? {"text":?"歡快",?"x":?140,?"y":?270},
? ? ? ? {"text":?"勁爆",?"x":?60,?"y":?130},
? ? ? ? {"text":?"喜慶",?"x":?60,?"y":?200},
? ? ? ? {"text":?"自然",?"x":?60,?"y":?270}
? ? ]
? ??# 創(chuàng)建音樂選項文本
? ? music_texts = []
? ??for?option?in?music_options:
? ? ? ? text = gui.draw_text(
? ? ? ? ? ? x=option["x"], y=option["y"],
? ? ? ? ? ? text=option["text"],
? ? ? ? ? ? origin='center',
? ? ? ? ? ? font_size=14,
? ? ? ? ? ? angle=270
? ? ? ? )
? ? ? ? music_texts.append(text)
? ??# 創(chuàng)建選擇框
? ? rect_x =?125
? ? rect_y =?105
? ? music_rect = gui.draw_rect(
? ? ? ? x=rect_x, y=rect_y,
? ? ? ? w=30, h=50,
? ? ? ? width=3, color=(255,?0,?0)
? ? )
? ??# 當前選擇的音樂索引
? ? current_index =?0
? ? bgm_mapping = ["舒緩",?"田園",?"歡快",?"勁爆",?"喜慶",?"自然"]
? ??# 音樂選擇循環(huán)
? ??while?app_state.state ==?"select_music":
? ? ? ??# 處理A鍵按下 - 選擇下一個音樂
? ? ? ??if?button_a.is_pressed():
? ? ? ? ? ? time.sleep(0.2) ?# 防抖延時
? ? ? ? ? ??# 更新選擇索引
? ? ? ? ? ? current_index = (current_index +?1) %?len(bgm_mapping)
? ? ? ? ? ??# 計算新位置
? ? ? ? ? ??if?current_index <?3:
? ? ? ? ? ? ? ? rect_x =?125
? ? ? ? ? ??else:
? ? ? ? ? ? ? ? rect_x =?45? ??
? ? ? ? ? ? rect_y =?105?+ (current_index %?3) *?70
? ? ? ? ? ??# 更新選擇框位置
? ? ? ? ? ? music_rect.config(x=rect_x, y=rect_y)
? ? ? ? ? ??print(f"選擇音樂:?{bgm_mapping[current_index]}")
? ? ? ??# 處理B鍵按下 - 確認選擇
? ? ? ??if?button_b.is_pressed():
? ? ? ? ? ? selected_bgm = bgm_mapping[current_index]
? ? ? ? ? ??print(f"確認選擇:?{selected_bgm}")
? ? ? ? ? ??return?selected_bgm
? ??# 如果狀態(tài)改變但未選擇,返回默認值
? ??return?bgm_mapping[0]
classCozeAPI:
? ??"""Coze API 操作類"""
? ??def__init__(self, access_token):
? ? ? ??self.base_url =?"https://api.coze.cn/v1"
? ? ? ??self.access_token = access_token
? ? ? ??self.headers = {
? ? ? ? ? ??"Authorization":?f"Bearer?{access_token}"
? ? ? ? }
? ??def_upload_file(self, file_path):
? ? ? ??"""上傳單個文件到Coze"""
? ? ? ? url =?f"{self.base_url}/files/upload"
? ? ? ? filename = os.path.basename(file_path)
? ? ? ??try:
? ? ? ? ? ??# 檢查文件大小
? ? ? ? ? ? file_size = os.path.getsize(file_path)
? ? ? ? ? ??if?file_size >?512?*?1024?*?1024: ?# 512MB
? ? ? ? ? ? ? ??print(f"跳過?{filename}?(超過512MB限制)")
? ? ? ? ? ? ? ??returnNone
? ? ? ? ? ??# 上傳文件
? ? ? ? ? ??withopen(file_path,?'rb')?as?file_obj:
? ? ? ? ? ? ? ? files = {'file': (filename, file_obj)}
? ? ? ? ? ? ? ? response = requests.post(url, headers=self.headers, files=files)
? ? ? ? ? ? ? ? response.raise_for_status()
? ? ? ? ? ? ? ??# 檢查響應
? ? ? ? ? ? ? ? result = response.json()
? ? ? ? ? ? ? ??if?result.get('code') ==?0:
? ? ? ? ? ? ? ? ? ??print(f"上傳成功:?{filename}")
? ? ? ? ? ? ? ? ? ??return?result['data']
? ? ? ? ? ? ? ??else:
? ? ? ? ? ? ? ? ? ??print(f"上傳失敗:?{result.get('msg',?'未知錯誤')}")
? ? ? ? ? ? ? ? ? ??returnNone
? ? ? ??except?Exception?as?e:
? ? ? ? ? ??print(f"上傳?{filename}?時出錯:?{str(e)}")
? ? ? ? ? ??returnNone
? ??defupload_images(self, folder_path):
? ? ? ??"""批量上傳圖片文件到Coze"""
? ? ? ? image_data = []
? ? ? ? valid_extensions = ('.png',?'.jpg',?'.jpeg',?'.gif',?'.bmp',?'.webp')
? ? ? ??ifnot?os.path.exists(folder_path):
? ? ? ? ? ??raise?FileNotFoundError(f"文件夾不存在:?{folder_path}")
? ? ? ??# 獲取所有圖片文件
? ? ? ? image_files = [f?for?f?in?os.listdir(folder_path)?
? ? ? ? ? ? ? ? ? ? ??if?f.lower().endswith(valid_extensions)]
? ? ? ??
? ? ? ??ifnot?image_files:
? ? ? ? ? ??print("文件夾中沒有圖片文件")
? ? ? ? ? ??return?image_data
? ? ? ??print(f"發(fā)現(xiàn)?{len(image_files)}?張圖片準備上傳")
? ? ? ??# 批量上傳
? ? ? ??for?filename?in?image_files:
? ? ? ? ? ? file_path = os.path.join(folder_path, filename)
? ? ? ? ? ? result =?self._upload_file(file_path)
? ? ? ? ? ??if?result:
? ? ? ? ? ? ? ? image_data.append(result)
? ? ? ??return?image_data
? ??defrun_workflow(self, workflow_id, parameters):
? ? ? ??"""執(zhí)行工作流"""
? ? ? ? url =?f"{self.base_url}/workflow/run"
? ? ? ? data = {
? ? ? ? ? ??"workflow_id": workflow_id,
? ? ? ? ? ??"parameters": parameters
? ? ? ? }
? ? ? ??print("執(zhí)行工作流參數(shù):")
? ? ? ??print(json.dumps(data, indent=4, ensure_ascii=False))
? ? ? ??try:
? ? ? ? ? ? response = requests.post(
? ? ? ? ? ? ? ? url,
? ? ? ? ? ? ? ? headers=self.headers,
? ? ? ? ? ? ? ? data=json.dumps(data),
? ? ? ? ? ? ? ? timeout=60# 增加超時時間
? ? ? ? ? ? )
? ? ? ? ? ? response.raise_for_status()
? ? ? ? ? ? response_data = response.json()
? ? ? ? ? ??# 檢查響應狀態(tài)碼
? ? ? ? ? ??if?response_data.get('code') !=?0:
? ? ? ? ? ? ? ??print(f"工作流執(zhí)行失敗:?{response_data.get('msg')}")
? ? ? ? ? ? ? ??returnNone? ? ?
? ? ? ? ? ??print("工作流執(zhí)行結(jié)果:")
? ? ? ? ? ??print(json.dumps(response_data, indent=4, ensure_ascii=False))
? ? ? ? ? ??return?response_data
? ? ? ??except?requests.exceptions.RequestException?as?e:
? ? ? ? ? ??print(f"工作流請求失敗:?{str(e)}")
? ? ? ? ? ??returnNone

defupload(BGM):
? ??"""上傳圖片并執(zhí)行工作流生成視頻二維碼"""
? ??print("開始上傳和處理流程")
? ??# 初始化API
? ? api = CozeAPI(COZE_TOKEN)
? ??try:
? ? ? ??# 批量上傳照片
? ? ? ??print("n===== 上傳照片 =====")
? ? ? ? app_state.caption_text.config(text="正在上傳照片")
? ? ? ? audio.play("upload_imging.mp3")
? ? ? ? upload_results = api.upload_images(IMAGE_FOLDER)
? ? ? ??ifnot?upload_results:
? ? ? ? ? ??print("沒有成功上傳的照片,終止處理")
? ? ? ? ? ? app_state.caption_text.config(text="上傳失敗,無照片")
? ? ? ? ? ??returnFalse
? ? ? ??# 準備圖片輸入
? ? ? ? image_entries = [{"file_id": item['id']}?for?item?in?upload_results]
? ? ? ??print(f"n準備處理的圖片:?{len(image_entries)}張")
? ? ? ? app_state.caption_text.config(text=f"已上傳{len(image_entries)}張照片 等待合成")
? ? ? ? audio.play("img_video.mp3")
? ? ? ??# 執(zhí)行工作流
? ? ? ??print("n===== 執(zhí)行工作流 =====")
? ? ? ? workflow_result = api.run_workflow(
? ? ? ? ? ? workflow_id=WORKFLOW_ID,
? ? ? ? ? ? parameters={
? ? ? ? ? ? ? ??"img_input": image_entries,
? ? ? ? ? ? ? ??"text_input": BGM
? ? ? ? ? ? }
? ? ? ? )
? ? ? ??ifnot?workflow_result:
? ? ? ? ? ??print("工作流執(zhí)行失敗")
? ? ? ? ? ? app_state.caption_text.config(text="工作流執(zhí)行失敗")
? ? ? ? ? ??returnFalse
? ? ? ??# 處理工作流結(jié)果
? ? ? ??if'data'in?workflow_result:
? ? ? ? ? ? workflow_output = json.loads(workflow_result['data'])
? ? ? ? ? ??if'data'in?workflow_output:
? ? ? ? ? ? ? ??# 解碼二維碼圖片
? ? ? ? ? ? ? ? bytes_data = base64.b64decode(workflow_output['data'])
? ? ? ? ? ? ? ??# 創(chuàng)建臨時文件路徑
? ? ? ? ? ? ? ? qr_path = os.path.join(IMAGE_FOLDER,?f"qrcode_{int(time.time())}.png")
? ? ? ? ? ? ? ??# 保存二維碼圖片
? ? ? ? ? ? ? ??withopen(qr_path,?"wb")?as?img_file:
? ? ? ? ? ? ? ? ? ? img_file.write(bytes_data)
? ? ? ? ? ? ? ??print(f"二維碼已保存至:?{qr_path}")
? ? ? ? ? ? ? ??# 旋轉(zhuǎn)并顯示二維碼
? ? ? ? ? ? ? ? img = Image.open(qr_path)
? ? ? ? ? ? ? ? img = img.transpose(Image.ROTATE_270)
? ? ? ? ? ? ? ??# 更新UI
? ? ? ? ? ? ? ? app_state.caption_text.config(text="視頻已合成 掃碼觀看")
? ? ? ? ? ? ? ? app_state.camera_image.config(x=180, y=100, w=180, h=230,image=img) ? ? ? ? ? ??
? ? ? ? ? ? ? ??# 播放完成提示音
? ? ? ? ? ? ? ? audio.play("video_finish.mp3")
? ? ? ? ? ? ? ??returnTrue? ? ?
? ? ? ??print("未獲取到有效的二維碼數(shù)據(jù)")
? ? ? ? app_state.caption_text.config(text="未獲取到二維碼")
? ? ? ??returnFalse? ? ?
? ??except?Exception?as?e:
? ? ? ??print(f"程序異常:?{str(e)}")
? ? ? ? app_state.caption_text.config(text=f"處理出錯:?{str(e)}")
? ? ? ??returnFalse
? ??finally:
? ? ? ??print("n上傳處理流程完成")

defhandle_take_photo():
? ??"""處理拍照操作"""
? ??# 拍照并更新照片計數(shù)
? ? app_state.photo_count = take_pictures()
? ? app_state.state =?""
? ? gui.clear()
? ? first_page()
? ? app_state.caption_text.config(text=f"已拍攝{app_state.photo_count}張照片,點擊選擇音樂")
? ? audio.play("photoOk.mp3")
defhandle_select_music():
? ??"""處理音樂選擇操作"""
? ??# 提示用戶選擇音樂
? ? app_state.caption_text.config(text="A鍵選擇 B鍵返回")
? ? audio.play("photo_music.mp3")
? ??# 選擇音樂
? ? app_state.bgm = music()
? ??print(f"選擇的音樂:?{app_state.bgm}")
? ??# 更新UI
? ? app_state.state =?""
? ? gui.clear()
? ? first_page()
? ??# 顯示狀態(tài)信息并播放語音提示
? ? app_state.caption_text.config(text=f"拍攝{app_state.photo_count}張照片 選擇主題{app_state.bgm}")
? ? generate_audio(f"拍攝{app_state.photo_count}張照片 選擇的音樂主題是{app_state.bgm},點擊文件圖標可上傳文件合成視頻")

defhandle_upload():
? ??"""處理上傳操作"""
? ? upload(app_state.bgm)
? ? app_state.state =?""
# ============== 主循環(huán)函數(shù) ==============
defselect_mode():
? ??"""主狀態(tài)循環(huán)"""
? ??# 設置回調(diào)函數(shù)映射
? ? state_handlers = {
? ? ? ??"take_photo": handle_take_photo,
? ? ? ??"select_music": handle_select_music,
? ? ? ??"upload": handle_upload
? ? }
? ??# 主循環(huán)
? ??while?button_b.is_pressed() !=?1:
? ? ? ??if?app_state.state?in?state_handlers:
? ? ? ? ? ??# 執(zhí)行對應的狀態(tài)處理函數(shù)
? ? ? ? ? ? handler = state_handlers[app_state.state]
? ? ? ? ? ??try:
? ? ? ? ? ? ? ? handler()
? ? ? ? ? ??except?Exception?as?e:
? ? ? ? ? ? ? ??print(f"狀態(tài)處理錯誤:?{str(e)}")
? ? ? ? ? ? ? ? app_state.caption_text.config(text="操作出錯,請重試")
? ? ? ? ? ? ? ? app_state.state =?""
# ============== 主入口 ==============
if?__name__ ==?'__main__':
? ??# 初始化UI
? ? first_page()
? ??# 播放啟動音
? ? audio.play("start.mp3")
? ??# 啟動主循環(huán)
? ? select_mode()

寫在最后

通過這個項目,我成功地將開源硬件與AI技術相結(jié)合,制作出了一個能夠獨立完成定格動畫拍攝與制作的智能硬件設備。

本次創(chuàng)客作品延續(xù)了以往的流程模式,亮點在于Agent的加持讓創(chuàng)意得以輕松實現(xiàn),為作品賦予獨特魅力。另一方面,隨著大模型的廣泛應用,其信息滯后、無法調(diào)用API接口以及易出現(xiàn)幻覺等缺陷也逐漸凸顯。為解決這些問題,Agent技術應運而生,而Coze平臺憑借其起步早、生態(tài)豐富的優(yōu)勢,成為了該領域的佼佼者。它極大地降低了技術開發(fā)門檻,讓普通人也能輕松搭建應用,實現(xiàn)技術的普及化。

本次項目雖是開源硬件和Agent工具的小型驗證,但還是展現(xiàn)出了開源硬件和Agent技術在教育和創(chuàng)客領域的巨大潛力。AI不是替代創(chuàng)造的工具,而是讓孩子專注創(chuàng)意的翅膀,我們可以通過工作流制作智能體,讓智能硬件專注于穩(wěn)定調(diào)用,其余復雜功能由工作流完成。以AI小智為例,利用Coze平臺的Agent技術,不僅可以輕松復刻,還能讓人們在搭建過程中熟悉技術原理,提升動手能力和創(chuàng)新思維。

在未來,我將繼續(xù)探索更多開源硬件與AI技術的結(jié)合方式,分享更多有趣、實用的創(chuàng)意項目。也歡迎感興趣的朋友一起加入,讓更多人感受到AI技術的魅力和開源硬件的樂趣!

造物讓生活更美好,我們下期再見

相關推薦