ssd 外接盒,對拷 (目標)
galileo.com.tw/DMC322D.html
伽利略 USB3.2 20Gbps M.2雙協議 對拷機
一張圖片(jpg or png ......),一個音檔(mp3 or m4a ......),組成一個影片(mp4 [h.264+aac])
前往試試
index.php
<!doctype html>
<html lang="zh-Hant-TW">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>圖片+聲音→影片</title>
<style>
*
{
font-size: 24pt;
}
.classTitle
{
font-size: 36pt;
}
#divProgress
{
white-space: pre-wrap; /* 保留空格和換行,長行自動換行 */
word-wrap: break-word; /* 避免超長單字溢出 */
font-family: monospace; /* 可選,看起來像 pre */
}
</style>
</head>
<body>
<div class="classTitle">圖片+聲音→影片</div>
<br>
<form class="classFormMaster" name="frm20251020102957" id="frm20251020102957" method="post" action="convert2mp4.php" enctype="multipart/form-data">
<div>
<label for="fileImage">圖片</label>
<input type="file" name="fileImage" id="fileImage" accept="image/*" required>
</div>
<div>
<label for="fileAudio">聲音</label>
<input type="file" name="fileAudio" id="fileAudio" accept="audio/*" required>
</div>
<input type="hidden" name="txtUniq" id="txtUniq" value="">
<button type="button" id="btnSubmit" onclick="checkAndSubmit_master();">送出</button>
</form>
<div id="divProgress"></div>
<script>
function gebi(strId)
{
return document.getElementById(strId);
}
function checkAndSubmit_master()
{
gebi('btnSubmit').setAttribute("disabled",true);
gebi('txtUniq').value="progress-"+Math.random().toString(36).substr(2, 16)+".txt";
while(true)
{
if(gebi('fileImage').files.length<1)
{
alert("抱歉,您還沒有選擇圖片喔!請修正後再上傳,謝謝!");
break;
}
if(gebi('fileAudio').files.length<1)
{
alert("抱歉,您還沒有選擇聲音喔!請修正後再上傳,謝謝!");
break;
}
gebi('frm20251020102957').submit();
gebi("divProgress").innerHTML="上傳中,請耐心等待,謝謝!";
showProgress();
return;
}
gebi('btnSubmit').removeAttribute("disabled");
}
async function showProgress()
{
let strFilename="tmp/"+gebi('txtUniq').value;
let oRes=await fetch(strFilename+"?rand=" + Math.random());
let strTxt = await oRes.text();
console.log((new Date()).toLocaleString());
if(strTxt.indexOf("progress=end")!=-1)
{
gebi('divProgress').innerHTML="";
gebi('btnSubmit').removeAttribute("disabled");
}
else if(strTxt.indexOf("404 Not Found")!=-1)
{
gebi('divProgress').innerHTML="上傳中,請稍候~~~"+(new Date()).toLocaleString();
window.setTimeout(showProgress, 1000);
}
else
{
var iPtr=strTxt.lastIndexOf('frame=');
if(iPtr!=-1)
{
strTxt=strTxt.substring(iPtr);
}
gebi('divProgress').textContent=strTxt;
window.scrollTo(0, document.body.scrollHeight);
window.setTimeout(showProgress, 2000);
}
}
</script>
</body>
</html>
convert2mp4.php
<?php
//$strTmpDir=sys_get_temp_dir();
$strTmpDir=rtrim(dirname(realpath(__FILE__)), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . "tmp";
$strExtImage=pathinfo($_FILES['fileImage']['name'], PATHINFO_EXTENSION);
$strExtAudio=pathinfo($_FILES['fileAudio']['name'], PATHINFO_EXTENSION);
$strImage=$strTmpDir . "/" . uniqid() . "." . $strExtImage;
$strAudio=$strTmpDir . "/" . uniqid() . "." . $strExtAudio;
$strProgressFilename=$strTmpDir . "/" . $_REQUEST['txtUniq'];
$booImage=move_uploaded_file($_FILES['fileImage']['tmp_name'], $strImage);
$booAudio=move_uploaded_file($_FILES['fileAudio']['tmp_name'], $strAudio);
$strVideo=$strTmpDir . "/" . uniqid() . ".mp4";
// 檢查檔案
if (!file_exists($strImage) || !file_exists($strAudio))
{
die("缺少檔案,終止執行!(上傳檔案上限為100MB)");
}
// ffmpeg 指令
$strCmd = sprintf(
'ffmpeg -loop 1 -i %s -i %s -vf "scale=\'trunc(min(iw\\,1280)/2)*2:trunc(min(ih\\,720)/2)*2\',format=yuv420p" -c:v libx264 -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -shortest -progress %s %s 2>&1',
escapeshellarg($strImage),
escapeshellarg($strAudio),
escapeshellarg($strProgressFilename),
escapeshellarg($strVideo)
);
file_put_contents($strTmpDir . '/log.log', $strCmd . "\r\n" , FILE_APPEND);
// 執行 ffmpeg
exec($strCmd, $strLog, $strRet);
// 清除原始檔
@unlink($strImage);
@unlink($strAudio);
// 檢查結果
if(file_exists($strVideo)==false)
{
header("Content-Type: text/plain; charset=utf-8");
echo "影片產生失敗。\r\n";
echo htmlspecialchars(implode("\r\n", $strLog));
exit;
}
// 回傳下載
header('Content-Description: File Transfer');
header('Content-Type: video/mp4');
header('Content-Disposition: attachment; filename="result.mp4"');
header('Content-Length: ' . filesize($strVideo));
readfile($strVideo);
// 刪除暫存影片,與進度
@unlink($strVideo);
@unlink($strProgressFilename);
?>
windows10 + pyttsx3, 文字轉語音
import pyttsx3
list1=["1 小鎮","2 柿餅節","3 開始","4 靜靜的","5 新埔","6 因此","7 變得","8 熱鬧","9 走進小鎮","10 飄了過來","11 看到","12 屋前","13 屋後","14 都排了","15 遠看","16 一籃","17 陽光","18 金黃色","19 近看","20 可愛","21 每年九月","22 這裡","23 慢慢風乾","24 變成","25 香甜","26 遊客","27 除了","28 客家菜","29 也會買","30 送給","31 親朋好友","32 心想事成","33 事事如意"]
engine = pyttsx3.init()
# 列出所有可用語音
voices = engine.getProperty('voices')
for v in voices:
print(v.id)
# 選擇中文語音(依系統而定,通常包含 "ZH" 或 "Chinese")
for v in voices:
if "ZH" in v.id.upper() or "CHINESE" in v.name.upper():
engine.setProperty('voice', v.id)
break
engine.setProperty('rate', 180) # 語速
engine.setProperty('volume', 1.0) # 音量
for lst1 in list1:
#print(lst1)
engine.say(lst1)
engine.runAndWait()
input("press enter to continue")
Raspberry Pi 4 + vulkan
Raspberry Pi 4 環境下
sudo apt update
sudo apt full-upgrade
sudo apt install vulkan-tools mesa-vulkan-drivers
sudo apt install vulkan-validationlayers-dev
vkcube
(如果出現旋轉立方體,就代表 Vulkan 正常運作)
pip install vulkan
運用 python 讓電腦即時監聽,錄音,辨識成文字。當聽到 再見 時,程式就會停下來。----語音助理前哨站(opus)
import threading
import time
# pyenv shell 3.13.1
# sudo apt install portaudio19-dev
# pip install pyaudio
import pyaudio
import numpy as np
import wave
# pyenv shell 3.13.1
# pip3 install torch torchvision --index-url https://download.pytorch.org/whl/cpu
import torch
# pyenv shell 3.13.1
# sudo apt install ffmpeg
# pip install -U openai-whisper
import whisper
# pyenv shell 3.13.1
# pip install opencc
from opencc import OpenCC
# pip install soundfile
import soundfile
import subprocess
import io
iIndexWrite=0
iIndexRead=0
iCount=0
iMax=10
booStop=False
def getFDecibel(oAudioData):
oAudioData=oAudioData.astype(np.float32) # 轉換為浮點數,避免整數溢出
fRMS=np.sqrt(np.mean(np.square(oAudioData))) # 計算rms
if fRMS>0:
fDecibel=20*np.log10(fRMS+1e-10) # 避免 log(0) 錯誤
else:
fDecibel=-np.inf
return fDecibel
def getStrAduioFilename(iIndex):
return "rec_"+str(iIndex)+".ogg"
def threadA():
global iIndexWrite, iIndexRead, iCount, iMax, booStop
CHUNK=1024 # 單次讀取的樣本數
FORMAT=pyaudio.paInt16 # 音訊格式(16-bit)
CHANNELS=1 # 單聲道
RATE=16000 # 取樣率(Hz)
THRESHOLD_DB=70 # 觸發錄音的分貝閥值
SILENCE_DURATION=1 # 安靜維持幾秒後停止錄音
DROP_DURATION=2 # 長度不足就丟棄
CUT_DURATION=30 # 超過就截斷
oAudio=pyaudio.PyAudio()
oStream=oAudio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)
booRecording=False
oaAudioData=[]
iTotalSamples=0
fSilenceStart=None
print("開始聆聽")
try:
while booStop==False:
# 讀取音訊數據
oAudioData=np.frombuffer(oStream.read(CHUNK, exception_on_overflow=False), dtype=np.int16)
fDecibel=getFDecibel(oAudioData)
# print(f"目前分貝數:{fDecibel:.2f} dB")
if fDecibel>THRESHOLD_DB:
fSilenceStart=None # 重置靜音計時
if booRecording==False and fDecibel>THRESHOLD_DB:
print("偵測到聲音,開始錄音...")
booRecording=True
oaAudioData=[]
oaAudioData.append(oAudioData)
iTotalSamples=oAudioData.shape[0]
elif booRecording==True:
oaAudioData.append(oAudioData)
iTotalSamples=iTotalSamples+oAudioData.shape[0]
fSeconds=iTotalSamples/RATE
if fSilenceStart is None:
fSilenceStart=time.time() # 開始計算靜音時間
elif ((time.time()-fSilenceStart)>=SILENCE_DURATION) or (fSeconds>CUT_DURATION):
if fSeconds<DROP_DURATION:
print("不足"+str(DROP_DURATION)+"秒,不予儲存")
else:
if fSeconds>CUT_DURATION:
print("超過"+str(CUT_DURATION)+"秒,截斷錄音。("+str(fSeconds)+")")
else:
print("偵測到靜音,停止錄音。("+str(fSeconds)+")")
if iCount<iMax:
oTotalAudioData=np.concatenate(oaAudioData)
# 寫入 wav 到記憶體
oWaveBuffer=io.BytesIO()
soundfile.write(oWaveBuffer, oTotalAudioData, RATE, format="wav")
oWaveBuffer.seek(0)
# 壓縮成 Opus 存入記憶體
oOpusBuffer=io.BytesIO()
oProcess=subprocess.Popen(
['ffmpeg', '-i', 'pipe:0', '-c:a', 'libopus', '-b:a', '32k', '-f', 'ogg', 'pipe:1'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL
)
oOpusData,_=oProcess.communicate(input=oWaveBuffer.read())
oOpusBuffer.write(oOpusData)
oOpusBuffer.seek(0)
strFilename=getStrAduioFilename(iIndexWrite)
print(f"正在儲存錄音檔案:{strFilename}")
with open(strFilename, "wb") as oF:
oF.write(oOpusBuffer.getvalue())
print("錄音儲存完成!")
oWaveBuffer.close()
oOpusBuffer.close()
iIndexWrite=(iIndexWrite+1) % iMax
iCount=iCount+1
else:
print("空間不足,不予儲存")
booRecording=False
oaAudioData=[]
iTotalSamples=0
fSilenceStart=None
except KeyboardInterrupt:
booStop=True
print("手動結束聆聽")
# 停止並關閉音訊流
oStream.stop_stream()
oStream.close()
oAudio.terminate()
def threadB():
global iIndexWrite, iIndexRead, iCount, iMax, booStop
strDevice="cuda" if torch.cuda.is_available() else "cpu"
booCuda=True if strDevice=="cuda" else False
oModel=whisper.load_model("tiny").to(strDevice) # tiny, base, small, medium, large
oCC=OpenCC('s2t') # 's2t' 表示簡體轉繁體
print("使用裝置:"+strDevice)
while booStop==False:
if iCount>0:
strFilename=getStrAduioFilename(iIndexRead)
oResult=oModel.transcribe(strFilename, language="zh", fp16=booCuda) # 假如有cuda支援,用fp16=True會更快
strTraditionalText=oCC.convert(oResult["text"])
print("------")
print("辨識 "+strFilename+" 結果:"+strTraditionalText)
print("------")
iIndexRead=(iIndexRead+1) % iMax
iCount=iCount-1
if strTraditionalText.find("再見")!=-1:
print('Goodbye!')
booStop=True
time.sleep(0.1)
# 創建兩個執行緒
oThreadA=threading.Thread(target=threadA)
oThreadB=threading.Thread(target=threadB)
# 啟動執行緒
oThreadA.start()
oThreadB.start()
# 讓主執行緒保持運行
oThreadA.join()
oThreadB.join()
print('兩個執行緒都停下來了')