項目背景
大家好,我是旺仔爸爸,一名喜歡造物的創(chuàng)客奶爸,近三年未更新內容,不知道你們有沒有忘記這個賬號。
在當今數字化時代,AI技術正以驚人的速度改變著我們的生活與創(chuàng)作方式,這次我?guī)硪粋€和AI有點關聯的小項目,和大家分享。
在孩子的成長過程中,創(chuàng)意與想象力的培養(yǎng)至關重要。我的孩子在5歲時便對制作定格動畫產生了濃厚的興趣,他喜歡用積木搭建場景并拍攝定格動畫視頻。然而,每次都需要父母作為攝影師和剪輯師協助完成,這不僅耗時,也限制了孩子的自主創(chuàng)作。于是,我萌生了一個想法:能否制作一個智能設備,讓孩子能夠獨立完成定格動畫的拍攝與制作呢?這個想法成為了我創(chuàng)作這個項目的初衷。
創(chuàng)客解法:用智能硬件+Agent技術打造自動化設備,實現兒童獨立創(chuàng)作。
通過本次項目的制作,我發(fā)現開源硬件和Agent是一個非常不錯的搭檔,不管是在教育領域還是創(chuàng)客領域,都可以幫助我們實現很多以前沒辦法實現的創(chuàng)意。我會持續(xù)更新關于這方面的內容,大家可以關注一下,也歡迎有同樣興趣的同學一起交流
知識背景
本次我們的主題是定格動畫,首先我們了解一下什么是定格動畫
定格動畫,這種古老而迷人的藝術形式,起源于1907年,通過逐格拍攝對象并連續(xù)放映,創(chuàng)造出仿佛被賦予生命的奇妙畫面。無論是黏土偶、木偶還是混合材料的角色,都能在定格動畫的世界里活靈活現。它的制作原理簡單而直觀:拍攝多張照片,然后將這些照片連續(xù)播放,形成流暢的動畫效果。這種獨特的藝術形式,不僅能夠激發(fā)孩子們的創(chuàng)造力,還能培養(yǎng)他們的耐心與專注力。
設計思路
定格動畫簡單理解就是拍攝多張照片然后連續(xù)放映。能勝任這項工作的設備需要能看(攝像頭)、會聽說(麥克風、喇叭)、可以思考(處理數據),最好還能調用AI工具
具備這樣能力的設備有電腦,樹莓派(核桃派等),行空板M10,esp32等。
在眾多可勝任定格動畫拍攝與制作的設備中,行空板M10以其卓越的性能、豐富的功能和出色的便攜性脫穎而出。它不僅擁有堪比樹莓派的處理能力,還集成了觸摸屏、擴展接口,并預裝了多種Python第三方庫,極大地簡化了配置過程。此外,行空板M10的電源擴展版設計,使其在便攜性上更勝一籌,非常適合用于制作便攜式智能設備。
器材清單
方案確定后,開始設計
設計制作
人靠衣裳馬靠鞍,必須要有一個不錯的外觀結構才能拿得出手,沒準哪個奶爸媽看中潛力股就投了呢
圖紙設計
既然是拍攝照片,設計成一個相機的風格會更具象化,
整體是一個盒子結構,將行空板、喇叭固定在盒子中,攝像頭部分以一個鏡頭的造型結構來呈現,細節(jié)部分需要注意預留type-c接口、電源開關、三個按鍵的孔位,圖紙設計如下
加工零件
圖紙設計完成后,我們使用激光切割機把圖紙加工出來,加工完成后的零件如下圖所示
零件加工完成,下面將電子部件和結構零件進行組裝
組裝
組裝分兩部分,電路部分和零件部分
電路接線
本次作品電路設計沒有其他多余的外設,攝像頭鏈接至行空板M10的USB接口,為了節(jié)省空間,這里的USB端口需要選用一種側邊接線規(guī)格的。而藍牙功放板的供電和攝像頭共用即可,音頻播放是通過藍牙與行空板連接,這里連接USB口只是為了供電,藍牙功放板和行空板之間沒有進行數據傳輸
零件組裝
下面開始零件組裝,第一步將行空板和擴展板按照官方的說明組合起來
第二步,我們找出切好的木制前面板,將行空板和前面板組合在一起,隨后安裝側面邊框,根據按鍵、type-c、電源等接口調整邊框的安裝方向,需要注意的細節(jié)是,這里我們用到了幾個白色的塑料按鈕,在安裝側邊框是需要提前將按鈕預置進去
第三步,將藍牙功放和喇叭的電路連接,藍牙功放板使用行空板的USB供電
接著安裝攝像頭,攝像頭使用滾花銅柱固定在背板上
攝像頭固定后,給它裝上鏡頭外觀結構
最后來看一下成品吧
組裝完成,最后就是程序設計了,開始程序設計前,我們需要先理清楚思路
程序設計
編程思路
下圖是本次程序設計的基本思路,使用行空板M10調用攝像頭拍攝圖像,用戶根據拍攝場景選擇喜歡的音樂主題,之后上傳文件至Coze Agent工作流,等待合成視頻后將視頻鏈接地址以二維碼的形式返回給行空板,用戶可用手機掃碼觀看視頻,也可以將視頻以二維碼的形式或鏈接的形式傳播
理解了定格動畫生成器的工作流程,下面我們來逐步拆解學習每個部分
本次作品我們使用Mind+軟件中的Python模式來為行空板M10編寫程序,軟件可在mindplus.cc下載,關于軟件的使用方法可以參考官方教程,這里不再贅述
攝像頭調用
第一步,先來學習攝像頭的使用方法,調用攝像頭需要使用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 ??# 返回拍攝照片數量
if?__name__ ==?'__main__':
? ? take_photo()
運行之后會看到如下結果
在行空板M10的屏幕中出現了攝像頭的畫面,但方向似乎有些不太對,行空板默認是豎向顯示,而我們本次的作品需要把屏幕橫向顯示
這里我們需要在畫面展示之前使用下面的指令來將屏幕逆時針旋轉90°
frame = cv2.rotate(frame,cv2.ROTATE_90_COUNTERCLOCKWISE) #逆時針旋轉90度
cv2.imshow('Video Cam', frame)
調整之后畫面就顯示正常
接著我們解釋一下代碼中需要注意的部分。首先我們設置了圖片拍攝后的存放路徑,為了保持文件之間的條理性我們使用了絕對路徑來存放文件IMAGE_FOLDER = '/root/mindplus/M10/img/',與絕對路徑相對的相對路徑,兩種方式各有特點,絕對路徑從根目錄開始,不受當前目錄影響;而相對路徑指的是當前目錄,使用相對簡單
制作定格動畫需要保存拍攝的多張照片合成視頻,為來保證文件的唯一性,這里我們借用時間函數來命名文件
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
運行結果如下
上述程序封裝在一個take_photo()方便調用
為了讓程序能夠重復使用,上一次拍攝的照片不要影響下一次的視頻合成,我們這里需要在調用拍照程序之前將保存照片的文件夾中的文件清除,刪除文件的指令可以用os.unlink()或者os.remove()代碼如下,調用將該函數需要插在拍照之前
def?delete_files(folder_path):
? ??"""清空文件夾內所有內容(保留文件夾本身)"""
? ??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}")
運行結果如下
到目前,我們的作品就實現了能看功能,也就是具備了采集照片的能力
語音合成與播放
下面來實現會說的能力,本次作品的功能并不需要識別用戶說的內容,只需要將機器合成的內容播放出來即可。這里分兩部分來完成,第一步要實現語音合成,第二步再將合成的音頻文件播放出來。
先來實現語音合成,下面的代碼中使用了微軟的edge_tts實現語音識別,關于語音合成我比較了百度、訊飛、Pytts,最終我選擇了edge_tts,原因是它不需要設置API,音色比較豐富,在調用該庫時,同樣需要在終端安裝,輸入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多種語音,還支持調節(jié)語速、音調、和音量,感興趣的朋友可以查找資料嘗試一下,這里我們只需要選擇默認即可
使用語音合成函數可以生成一個mp3格式的音頻,拿到這個音頻文件,可以使用音頻播放指令將文件播放出來,播放音頻需要使用行空板自帶的Audio庫,指令如下
from unihiker import Audio
audio = Audio() #實例化音頻
audio.play(output_file)
這樣語音合成,語音播放的功能已經實現,這里提兩點避坑指南
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、語音合成的功能只能用來生成音頻文件,如要播放需要調用音頻播放指令,經測試發(fā)現,用edge_tts語音合成的響應速度雖說已經很不錯了,但還是會有一點點延時,如需提升響應效率,可提前生成要播放的內容,在需要時直接本地調用播放可大大縮短時間
做到能看會說之后,現在需要與Coze Agent進行交互了
交互
Coze工作流搭建
有的朋友可能會問,這里為什么要使用Coze呢,既然行空板性能和樹莓派類似,行空板自己就可以合成視頻呀。沒錯,確實是這樣,這里我們使用Coze來搭建Agent其實是因為新的AI工具出來后,我們選擇的一種相對來說比較合理的方案,之所以選擇Coze是因為目前它的智能體、插件等生態(tài)資源非常豐富,幾乎在一個平臺中只需要調用不同的插件就可以實現功能了,不用再像之前那樣調用各種平臺,查找各種資料,配置各種接口才能實現。其實現在可以做智能體的平臺也有很多,像dify、n8n、Fastgpt等都可以,其實邏輯原理是相通的,只是Coze的資源更豐富,學習起來更容易,在起步階段先以一個簡單的上手,后續(xù)遷移其他平臺也很快
在調用coze之前,我們要先在coze平臺搭建工作流
扣子是字節(jié)系的產品,平臺中大量的Agent應用可以嘗試體驗,國內地址:https://www.coze.cn/
下面我們來親自搭建一個工作流
點擊如下圖所示進入開發(fā)平臺,如需注冊需要用手機號或者字節(jié)系的賬號注冊
Coze的功能很多,依次按照如下圖所示步驟創(chuàng)建工作流。什么是工作流,為什么用工作流,工作流和Agent有什么關系?想必現在你有很多疑問,沒關系,這個我們先不管,保留著好奇先跟著做完,(暫且先簡單理解Agent和工作流是包含關系,作用都是為了補齊大模型的短板,輔助實現某些功能,關于概念的理解后面我們再解釋)
工作流創(chuàng)建完成后,可以看到一個只有開始和結束兩個節(jié)點的簡單工作流,該工作流的作用就是將輸入的內容原封不動的再輸出。不論是什么樣的工作流,都必須要有開始和結束節(jié)點,也就是輸入和輸出
如要實現特定的功能,只需在中間添加節(jié)點即可,如下圖所示,我們可以在開始和結束節(jié)點中間添加一個文本處理節(jié)點,作用是將輸入的內容進行拼接后輸出,每個中間節(jié)點都會有一個輸入和輸出,需要分別設置當前節(jié)點的輸入和輸出內容是什么,也就是圖中的input和output,這里的名稱是默認的,可以根據需求更改,數據通過數據流來傳輸,這里設置文本處理節(jié)點接收開始節(jié)點的input內容,將字符串拼接后的結果作為結束節(jié)點輸入的內容,當我們輸入hello+時,結果會輸出hello+world,通過以上測試就學會了工作流的基本搭建方法
下面我搭建視頻合成的工作流,視頻合成的工作流不算復雜,但也需要清晰拆解一下實現邏輯,工作流的輸入、輸出和中間節(jié)點如下
下面逐步實現,
第一步,實現背景音樂的生成
點擊添加節(jié)點搜索BGM關鍵詞,添加背景音樂庫,該節(jié)點的使用方法很簡單,只需要輸入音樂風格即可生成背景音樂的鏈接,關于節(jié)點的使用方法也可以點擊插件詳情了解,這里設置將開始節(jié)點的music_style作為輸入內容傳給音樂庫節(jié)點,再將輸出的bgm_url傳給結束節(jié)點,可以看到結果如下
掌握了背景音樂生成的工作流,其他的功能其實就很簡單了
第二步,實現單張圖片和音頻制作視頻片段
這里需要使用一個圖片制作視頻的插件節(jié)點,這個圖片合成視頻的插件來自51aigc.cc網站,其中api_token需要注冊賬號后在網站個人中心獲取,img_url可以找網絡圖片臨時測試,正式調用時使用行空板上傳的圖片,mp3_url來自上一個節(jié)點的輸出,設置完成后運行可看到輸出的結果為一個視頻鏈接
第三步,多個視頻片段合成完整視頻
上一步我們使用的是一個單張圖片和音頻合成視頻的插件,現在我們繼續(xù)使用這個視頻合成工具箱插件中的video_merging實現多個視頻合成完整視頻,如下圖所示,我們添加插件后,需要輸入api_token和video_urls這兩個參數,其中api_token和上一步的保持一致即可,video_urls是一個數組,用來存放每個視頻片段的url鏈接,這個鏈接正是來自上一步合成的視頻,并且每一張圖片生成視頻片段后對應一個鏈接,所以這里需要以數組的形式來接收這些鏈接
到這里,有的朋友可能會問,多個視頻片段的鏈接從哪里得到呢?其實很簡單,只需要將第二步的工作重復多次即可,這里需要用到批處理節(jié)點,通過重復執(zhí)行相同的工作,將制作的多個視頻片段的url鏈接以數組的形式輸出。想必大家已經發(fā)現了問題,第一步我們生成的是一個完整的背景音樂,如果是多張圖片,那豈不是需要將音頻切開分段,再與每張圖片合成視頻。的確是這樣,所以這里我們還需要一個音頻切割的插件,負責完成這項工作,如下圖所示,我們添加音頻切割插件,只需要輸入起止時間就可以將音頻切割,并以url鏈接的形式輸出,之后再將url鏈接與圖片合成視頻即可,只不過這里我們同樣需要用到批處理的方式來重復切割音頻。
這里的關鍵是要確定每段音頻的起止時間,需要使用一個代碼節(jié)點來完成這項特定功能,使用代碼節(jié)點只需要確定輸入和輸出的內容即可,它會自動完整,輸入的內容是圖片數組和音頻的時長,輸出的是圖片的數量,音頻時長和分段音頻的起止時間
Coze 工作流中除了有很多成熟的插件可以使用外,還提供了代碼節(jié)點,可以方便我們實現特殊功能,下面是本次計算音頻時長的節(jié)點代碼
async?defmain(args: Args) -> Output:
? ??# 從輸入參數中獲取音頻時長(微秒)和圖片列表
? ? params = args.params
? ? audio_duration_us = params['audio_duration']
? ? img_list = params['img_list']
? ??
? ??# 計算圖片數組的長度
? ? 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)
? ? ? ??
? ? ? ??# 轉換為微秒(整數運算避免浮點誤差)
? ? ? ? segment_duration_us =?int(segment_duration_sec *?1_000_000)
? ? ? ??
? ? ? ??# 生成音頻起止時間數組
? ? ? ??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)
? ? ? ? ? ??
? ? ? ? ? ??# 將微秒轉換為秒
? ? ? ? ? ? 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
經過以上三步,已經可以生成一個定格動畫視頻了,不過現在還需要將視頻的URL鏈接轉換成二維碼圖片,再將圖片轉化成base64編碼,輸出給行空板,前面的內容掌握后這幾步會非常簡單,按照下圖所示操作即可。設置完成后,試運行沒問題可以點擊發(fā)布,
發(fā)布后該工作流支持在Coze Agent中調用,也支持外部終端設備通過API接口來調用
Coze 工作流中的插件有官方插件、第三方插件以及個人用戶插件,關于插件的選擇,首選官方插件,其次是比較有實力保障的公司提供的插件,再次才是個人用戶的插件,插件根據功能的不同,分收費和免費,官方的插件免費贈送的節(jié)點用來測試基本夠用了,第三方的沒有大批量商用的花使用成本也非常低,本次我們使用的基本上是官方插件和51aigc團隊的插件,穩(wěn)定性是有保障的。關于Coze工作流搭建的環(huán)節(jié)我們已經介紹完了,Coze是一個非常不錯的學習AI Agent的平臺,本次我們只是簡單介紹了一下與本次項目相關的功能,這只是冰山一角,后面我會再通過不同的項目分享更多關于Coze平臺的使用方法
行空板調用工作流
Coze的工作流搭建完整,現在我們可以調用工作流的API接口進行測試,調用工作流的方法可以參考如下官方的鏈接
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和背景音樂的主題作為輸入參數傳入前面搭建好的工作流中,返回包含有視頻url的二維碼
在開始之前,我們需要做兩項準備工作
1、獲取Coze的個人令牌,也就是Token,在如下地址獲取即可,設置權限時可以選擇全部,注意這個token需要及時復制保存,不支持二次查看,如果忘記需要重新生成,之前的將會失效
https://www.coze.cn/open/oauth/pats
2、獲取工作流的ID,進入到工作流后,可以在網址欄中看到工作流的ID,記錄下來,方便調用
準備工作做好后,我們回到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ā)現?{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í)行工作流參數:")
? ? ? ??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í)行結果:")
? ? ? ? ? ??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交互的部分做了一個類函數,其中access_token為前面獲取到的個人令牌,我們將文件上傳分成了單個文件和批量文件上傳兩種,方便函數復用,運行程序,可以拿到如下測試結果,“code”為0表示測試成功,如返回的結果不是0,則需要根據“msg”信息調整。其中“data”數據既圖片上傳后的信息,“id”的內容在執(zhí)行工作流時使用。
{"code":0,"data":{"bytes":26451,"created_at":1750525076,"file_name":"online-shopping.png","id":"7518444294115147812"},"detail":{"logid":"202506220057559170147E27FFD06CBB7F"},"msg":""}
文件上傳支持多種常見的格式,單個文件大小不超過512M即可,我們在代碼中做了文件大小檢測,本次我們只需要上傳圖片即可,批量上傳后獲取到圖片ID可用來調用工作流生成視頻,調用執(zhí)行工作流函數run_workflow
時,除了傳入多張圖片ID的列表外,還需要將背景音樂主題以字符串的格式傳入。之后再將返回的二維碼顯示,這里我們封裝一個函數upload(BGM)來完成工作流的調用工作,代碼如下
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
? ? ? ??# 處理工作流結果
? ? ? ??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}")
? ? ? ? ? ? ? ??# 旋轉并顯示二維碼
? ? ? ? ? ? ? ? img = Image.open(qr_path)
? ? ? ? ? ? ? ? img = img.transpose(Image.ROTATE_270)
? ? ? ? ? ? ? ??returnTrue? ? ?
? ? ? ??print("未獲取到有效的二維碼數據")
? ? ? ??returnFalse? ? ?
? ??except?Exception?as?e:
? ? ? ??print(f"程序異常:?{str(e)}")
? ? ? ??returnFalse
? ??finally:
? ? ? ??print("n上傳處理流程完成")
主頁面狀態(tài)設置
其實,到目前為止,我們已經可以實現制作定格動畫視頻的功能了,只不過為了讓設備更好用,可以繼續(xù)優(yōu)化,設計一個UI交互界面,將拍攝照片、選擇音樂主題、上傳文件分別設置成不同的模塊供用戶調用。設置好的界面如下圖所示
要完成界面設計,需要提前準備一些圖標,將文件命名好后,以絕對路徑或相對路徑的形式調用
代碼如下
from?unihiker?import?GUI ??#導入包
from?pinpong.board?import?*
from?pinpong.extension.unihiker?import?*
gui=GUI() ?#實例化GUI類
deffirst_page():
? ??# 頁面初始化代碼...
? ??global?camera_image, Caption_text
? ??# 統一旋轉函數
? ??defload_rotated_image(path):
? ? ? ? img = Image.open(path)
? ? ? ??return?img.transpose(Image.ROTATE_270)
? ??# 加載并旋轉所有圖標
? ? 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(需要更新的參數名=值)用來更新控件,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,而我們本次的作品需要將行空板橫向顯示,所以不論是圖片還是文字都需要旋轉270°。而對應的坐標需要相應調整過來,關于行空板的坐標方向,以及不同控件的坐標基準點可以參考下圖
想必你已經發(fā)現了,界面中的圖標設置了回調函數,也就是說圖片是可以點擊的,通過函數iconclick(data)實現點擊不同的圖片切換不同的模式。同時在界面中的文字顯示部分由于選擇的模式不同,顯示的內容也會相應的切換,為此我們設置了狀態(tà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í)行的函數與模式狀態(tài)綁定起來。拍攝照片,工作流調用的函數在前面我們都已經設置完成,我們還需要設置一個選擇背景音樂主題的函數,代碼如下
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鍵確認并返回,效果如下
選擇音樂主題的函數設置完成后,我們還需要設置一個狀態(tài)選擇函數,設置當點擊不同的圖標后切換到對應的函數,按下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():
? ??"""處理拍照操作"""
? ??# 拍照并更新照片計數
? ? 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)函數 ==============
defselect_mode():
? ??"""主狀態(tài)循環(huán)"""
? ??# 設置回調函數映射
? ? 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)處理函數
? ? ? ? ? ? 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
? ??# 統一旋轉函數
? ??defload_rotated_image(path):
? ? ? ? img = Image.open(path)
? ? ? ??return?img.transpose(Image.ROTATE_270)
? ??# 加載并旋轉所有圖標
? ? 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):
? ??"""清空文件夾內所有內容(保留文件夾本身)"""
? ??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)?#逆時針旋轉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ā)現?{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í)行工作流參數:")
? ? ? ??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í)行結果:")
? ? ? ? ? ??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
? ? ? ??# 處理工作流結果
? ? ? ??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}")
? ? ? ? ? ? ? ??# 旋轉并顯示二維碼
? ? ? ? ? ? ? ? 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("未獲取到有效的二維碼數據")
? ? ? ? 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():
? ??"""處理拍照操作"""
? ??# 拍照并更新照片計數
? ? 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)函數 ==============
defselect_mode():
? ??"""主狀態(tài)循環(huán)"""
? ??# 設置回調函數映射
? ? 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)處理函數
? ? ? ? ? ? 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技術相結合,制作出了一個能夠獨立完成定格動畫拍攝與制作的智能硬件設備。
本次創(chuàng)客作品延續(xù)了以往的流程模式,亮點在于Agent的加持讓創(chuàng)意得以輕松實現,為作品賦予獨特魅力。另一方面,隨著大模型的廣泛應用,其信息滯后、無法調用API接口以及易出現幻覺等缺陷也逐漸凸顯。為解決這些問題,Agent技術應運而生,而Coze平臺憑借其起步早、生態(tài)豐富的優(yōu)勢,成為了該領域的佼佼者。它極大地降低了技術開發(fā)門檻,讓普通人也能輕松搭建應用,實現技術的普及化。
本次項目雖是開源硬件和Agent工具的小型驗證,但還是展現出了開源硬件和Agent技術在教育和創(chuàng)客領域的巨大潛力。AI不是替代創(chuàng)造的工具,而是讓孩子專注創(chuàng)意的翅膀,我們可以通過工作流制作智能體,讓智能硬件專注于穩(wěn)定調用,其余復雜功能由工作流完成。以AI小智為例,利用Coze平臺的Agent技術,不僅可以輕松復刻,還能讓人們在搭建過程中熟悉技術原理,提升動手能力和創(chuàng)新思維。
在未來,我將繼續(xù)探索更多開源硬件與AI技術的結合方式,分享更多有趣、實用的創(chuàng)意項目。也歡迎感興趣的朋友一起加入,讓更多人感受到AI技術的魅力和開源硬件的樂趣!
造物讓生活更美好,我們下期再見