开发一个 Streamlit 录音组件(二)——支持 iOS 页面

上一篇文章用 Streamlit 写了一个录音按钮的组件,实现了按下去时开始录音、放开结束录音的功能。但是只支持桌面端网页用鼠标点击,这次对齐进行扩展,使其能够实现在手机端按下录音的功能。 Touch 事件 在桌面端监听的是鼠标的 mousedown/mouseup 事件,但是在移动端则是手指触屏屏幕的事件 touchstart/touchend,因此需要修改按钮的监听事件。另外之前采用样式中 :hover 来实现按下时按钮颜色的变化,现在使用一个变量来控制 <template> <div class="button-container"> <button @touchstart="startRecording" @touchcancel="stopRecording" @touchend="stopRecording" @mousedown="startRecording" @mouseup="stopRecording" @mouseleave="stopRecording" class="red-round-button" :style="{ backgroundColor: buttonColor }" ></button> </div> <audio ref="audioPlayer" controls class="audio-player"></audio> </template> <script setup> ... const buttonColor = ref('red'); const dynamicStyles = computed(() => { return { width: props.args.width, height: props.args.height, backgroundColor: buttonColor }; }); ... </script> 另外,移动端长按按钮的话会唤出 Text Selection,需要取消在监听时间时取消掉,详见 Prevent text selection on tap and hold on iOS 13 / Mobile Safari function startRecording(event) { event.preventDefault(); ... } function stopRecording(event) { event.preventDefault(); ... } 录音格式兼容 由于 iOS 的浏览器录制的音频格式与桌面端浏览器不一样(参考 MediaRecorder: isTypeSupported() static method),需要在代码中先判断平台,再根据平台决定录音格式,其中桌面 Chrome 格式为 webm/opus,iOS Safari 为 mp4/aac,然后要将对应格式传回组件返回值 ...

March 29, 2024

开发一个 Streamlit 录音组件

用 Vue 写一个录音组件 先写一个 Vue 组件,用于录音功能。以下是一个简单的录音组件示例: <template> <div class="button-container"> <button @mousedown="startRecording" @mouseup="stopRecording" @mouseleave="stopRecording" class="red-round-button"></button> </div> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue'; const mediaRecorder = ref<MediaRecorder | null>(null); const audioChunks = ref<Blob[]>([]); async function startRecording() { if (!navigator.mediaDevices || !window.MediaRecorder) { alert('Media Devices API or MediaRecorder API not supported in this browser.'); return; } const audioConstraints = { audio: { sampleRate: 16000, channelCount: 1, volume: 1.0 } }; try { const stream = await navigator.mediaDevices.getUserMedia(audioConstraints); mediaRecorder.value = new MediaRecorder(stream); audioChunks.value = []; mediaRecorder.value.ondataavailable = (event) => { audioChunks.value.push(event.data); }; mediaRecorder.value.start(); } catch (error) { console.error('Error accessing the microphone', error); } } function stopRecording() { if (mediaRecorder.value && mediaRecorder.value.state !== 'inactive') { mediaRecorder.value.stop(); mediaRecorder.value.onstop = async () => { const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm' }); const audioUrl = URL.createObjectURL(audioBlob); const audio = new Audio(audioUrl); await audio.play(); }; } } onMounted(() => { // You can perform actions after the component has been mounted }); onUnmounted(() => { // Perform cleanup tasks, such as stopping the media recorder if it's still active if (mediaRecorder.value) { mediaRecorder.value.stop(); } }); </script> <style scoped> .button-container { display: flex; justify-content: center; align-items: center; height: 100vh; /* This assumes you want to center the button vertically in the viewport */ } .red-round-button { background-color: red; border: none; color: white; padding: 10px 20px; border-radius: 50%; /* This creates the round shape */ cursor: pointer; outline: none; font-size: 16px; /* Adjust width and height to make the button round, they must be equal */ width: 50px; height: 50px; /* Optional: Add some transition for interactions */ transition: background-color 0.3s; } .red-round-button:hover { background-color: darkred; } </style> 注意 ...

March 25, 2024

Python 语音录制与识别

本文介绍一些 Python 中常用的语音能力的包,以及如何通过调用云服务商的 API 进行语音识别 录音 主要使用 pyaudio 包,它可以以字节流的方式录制/播放音频 安装: pip install pyaudio 列出可以录音的设备 import pyaudio p = pyaudio.PyAudio() # Get the number of audio I/O devices devices = p.get_device_count() for i in range(devices): device_info = p.get_device_info_by_index(i) if device_info.get('maxInputChannels') > 0: print(f"{device_info.get('index')}: {device_info.get('name')}") 开始录音 5 秒,这里将录到的音频存到一个 io.BytesIO 对象中 import io FORMAT = pyaudio.paInt16 # format of audio samples CHANNELS = 1 # audio channels(1: mono, 2: stereo) RATE=44100 # sample rate CHUNK=1024 # number of frames per buffer RECORD_SECONDS = 5 p = pyaudio.PyAudio() stream = p.open( format=FORMAT, # format of audio samples channels=CHANNELS, # audio channels(1: mono, 2: stereo) rate=RATE, # sample rate frames_per_buffer=CHUNK, # number of frames per buffer input=True, ) print("Recording...") buffer = io.BytesIO() for _ in range(0, int(RATE / CHUNK * RECORD_SECONDS)) data = stream.read(CHUNK) buffer.write(data) stream.stop_stream() stream.close() p.terminate() 保存音频文件 使用标准库中的 wave 包将音频字节保存到 wav 文件中,它会将 wav 格式写入文件头部,详见文档:The Python Standard Library - wave ...

March 22, 2024

用 Streamlit 做几个网页快捷小工具

在日常工作中,经常需要时间戳转化、base64 编码/解码等操作。之前一般通过搜索引擎搜索,可以找到相应工具的页面。现在有了 Streamlit ,可以快速制作出对应功能的网页应用。例如以下的一些例子 安装: pip install streamlit Streamlit 脚本是一个从上到下执行的命令流,页面中控件值的改变会使得脚本重新执行 运行 streamlit 脚本: streamlit run main.py JSON 格式化 主要用到了以下控件 st.markdown:展示 Markdown 格式的文本,可以用展示标题等 st.text_area:输入文本框 一个文本框用于输入原始 JSON 数据,一个文本框展示格式化后的 JSON import streamlit as st import json def _format(s: str) -> str: try: pretty = json.dumps(json.loads(s), indent=4) except Exception as e: st.error(f'Error: {e}') pretty = '' return pretty st.markdown('# JSON Formatter') text_in = st.text_area('Input', '{}', height=100) text_out = _format(text_in) st.text_area(label='Output', value=text_out, height=600) 也可以使用控件 st.json ,自带格式化等功能 text_in = st.text_area('Input', '{}', height=100) st.json(json.loads(text_in)) Base64 编码/解码 可以选择是编码还是解码,比上面的例子多一个选择控件 ...

March 15, 2024

基于 Python 后端的聊天软件机器人开发

大部分聊天软件的机器人自动回复消息流程 sequenceDiagram participant 用户 participant 聊天软件后台 participant 机器人后台 聊天软件后台 ->> 机器人后台: 向回调地址发送验证消息 机器人后台 ->> 聊天软件后台: 回复 用户 ->> 聊天软件后台: 发送消息 聊天软件后台 ->> 机器人后台: 向回调地址推送消息 机器人后台 ->> 聊天软件后台: 确认收到 机器人后台 ->> 聊天软件后台: 请求 Access Token 聊天软件后台 ->> 机器人后台: Access Token 机器人后台 ->> 聊天软件后台: 调用发送消息的接口 聊天软件后台 ->> 用户: 发送消息 QQ 机器人 文档:QQ 机器人 - 简介 控制台:QQ 开放平台 申请流程 在 QQ 开放平台注册账号,可以选“个人主体入驻” 创建应用 -> 创建机器人 开发设置 -> 记录 APP ID、APP Secret 沙箱配置 -> 将测试频道添加到沙箱环境 部署后台 使用 python SDK,Github - botpy ...

February 7, 2024

Python 调度相关包的使用

schedule 使用起来比较简单的一个包 安装: pip install schedule 具体用法: import schedule # add schedule job schedule.every(10).seconds.do(lambda: print("running")) # run scheduler while True: schedule.run_pending() time.sleep(1) 运行带有参数的 job def func(name: str): print(f"My name is {name}") schedule.every(5).seconds.do(func, name="Tom") while True: schedule.run_pending() time.sleep(1) Apscheduler 一个功能更为完整的包 安装: pip install apscheduler 一些基本概念: Triggers:任务触发逻辑 cron:cron 格式触发 interval:固定时间间隔触发 date:在某固定日期触发一次 combine:组合条件触发 Scheduler BlockingScheduler: 阻塞式,当程序只运行这个 scheduler 时使用 BackgroundScheduler:调度器在后台运行 Executor ThreadPoolExecutor:默认使用多线程执行器 ProcessPoolExecutor:如果是 CPU 密集型任务可以使用多进程执行器 Job store:如果任务调度信息存在内存中,当程序退出后会丢失,可以其他存储器进行持久化存储 MemoryJobStore: 默认使用内存存储 SQLAlchemyJobStore MongoDBJobStore etc. 创建 scheduler ...

February 7, 2024

Environment

安装 Python 包 你的 Python 包都装到哪了? 假设当前 Python 解释器的路径是 $path_prefix/bin/python,那么你启动 Python 交互环境或者用这个解释器运行脚本时,会默认寻找以下位置 $path_prefix/lib(标准库路径) $path_prefix/lib/pythonX.Y/site-packages(三方库路径,X.Y 是对应 Python 的主次版本号,如 3.7, 2.6) 当前工作目录(pwd命令的返回结果) 几个有用的函数 sys.executable:当前使用的 Python 解释器路径 sys.path:当前包的搜索路径列表 sys.prefix:当前使用的 $path_prefix 除此之外,还在以在命令行中运行 python -m site,会打印出当前 Python 的一些信息,包括搜索路径列表。 使用环境变量添加搜索路径 如果你的包的路径不存在上面列出的搜索路径列表里,可以把路径加到 PYTHONPATH 环境变量里 虚拟环境 虚拟环境就是为了隔离不同项目的依赖包,使他们安装到不同的路径下,以防止依赖冲突的问题。理解了 Python 是如何安装包的机制之后就不难理解虚拟环境(virtualenv, venv模块)的原理。其实,运行virtualenv myenv会复制一个新的 Python 解释器到myenv/bin下,并创建好myenv/lib,myenv/lib/pythonX.Y/site-packages等目录(venv模块不是用的复制,但结果基本一样)。执行source myenv/bin/activate以后会把myenv/bin塞到PATH前面,让这个复制出来的 Python 解释器最优先被搜索到。这样,后续安装包时,$path_prefix就会是myenv了,从而实现了安装路径的隔离。 运行 Python 脚本 运行一个子目录中某脚本的代码,应该用 python -m <module_name>。python -m 后面的参数是(以 . 分隔的)模块名,而不是路径名。 pip 运行 pip 有两种方式: pip ... python -m pip ... 第一种方式和第二种方式大同小异,区别是第一种方式使用的 Python 解释器是写在 pip 文件的 shebang 里的,一般情况下,如果你的 pip 路径是 $path_prefix/bin/pip,那么 Python 路径对应的就是 $path_prefix/bin/python。如果你用的是 Unix 系统则 cat $(which pip) 第一行就包含了 Python 解释器的路径。第二种方式则显式地指定了 Python 的位置。 ...

January 1, 2000

Libraries

任务调度 schedule install pip install schedule usage import schedule # add schedule job schedule.every(10).seconds.do(lambda: print("running")) # run scheduler while True: schedule.run_pending() time.sleep(1) add job with parameters def func(name: str): print(f"My name is {name}") schedule.every(5).seconds.do(func, name="Tom") while True: schedule.run_pending() time.sleep(1) Apscheduler Install pip install apscheduler Triggers:任务触发逻辑 cron:cron 格式触发 interval:固定时间间隔触发 date:在某固定日期触发一次 combine:组合条件触发 Scheduler BlockingScheduler: 阻塞式,当程序只运行这个 scheduler 时使用 BackgroundScheduler:调度器在后台运行 Executor ThreadPoolExecutor:默认使用多线程执行器 ProcessPoolExecutor:如果是 CPU 密集型任务可以使用多进程执行器 Job store:如果任务调度信息存在内存中,当程序退出后会丢失,可以其他存储器进行持久化存储 MemoryJobStore: 默认使用内存存储 SQLAlchemyJobStore MongoDBJobStore etc. 创建 scheduler ...

January 1, 2000

Python

Command # print version python -V # run python command python -c "print('Hello world!')" Python Files Header #!/usr/bin/python # -*- coding: utf-8 -*- Module A python file is a module main.py database.py const.py import module # method 1: import module import database client = database.Client() # method 2: import class from module from database import Client run a module as script python -m module_name # if the module is in parent/child/module_name.py python -m parent.child.module_name Package A folder of python files is a package ...

January 1, 2000

Scraper

Scraper [TOC] urllib Python built-in lib for web requesting Import from urllib.request import urlopen from urllib.request import urlretrieve from urllib.error import HTTPError Open url page = urlopen(URL) Requests HTTP for human Import import requests get/post r = requests.get(URL) r = requests.post(URL) Add Headers headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36', 'Accept': 'text/html, application/xhtml+xml, application/xml; q=0.9, img/webp,*/*; q=0.8', 'Host': 'www.zhihu.com', 'Referer': 'https://www.zhihu.com/'} r = requests.get(URL, headers=headers) Add cookies cookies = dict(cookies_are='working') r = requests.get(URL, cookies=cookies) # using cookie jar jar = requests.cookies.RequestsCookieJar() jar.set('tasty_cookie', 'yum', domain='httpbin.org', path='/cookies') jar.set('gross_cookie', 'blech', domain='httpbin.org', path='/elsewhere') r = requests.get(url, cookies=jar) Check results ...

January 1, 2000