先写一个 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/wav' });
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>
注意
项目结构可以参考模板库
在封装过程中,我们需要做以下几点改动:
<template>
<div class="button-container">
<button @mousedown="startRecording" @mouseup="stopRecording" @mouseleave="stopRecording" class="record-button" :style="dynamicStyles"></button>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted , computed } from 'vue';
import { Streamlit } from "streamlit-component-lib"
import { useStreamlit } from "./streamlit"
const props = defineProps(['args'])
const dynamicStyles = computed(() => {
return {
width: props.args.width,
height: props.args.height,
};
});
useStreamlit();
const mediaRecorder = ref(null);
const audioChunks = ref([]);
async function startRecording() {
if (!navigator.mediaDevices || !window.MediaRecorder) {
alert('Media Devices API or MediaRecorder API not supported in this browser.');
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({audio: true});
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 function() {
const audioBlob = new Blob(audioChunks.value, { type: 'audio/wav' });
const reader = new FileReader();
reader.onload = function(event) {
if (event.target && event.target.result) {
const dataUrl = event.target.result;
const base64 = dataUrl.split(',')[1]; // Extract the base64 part
Streamlit.setComponentValue(base64);
} else {
console.log('Failed to read Blob as base64');
}
};
reader.readAsDataURL(audioBlob);
};
}
}
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; /* center the button vertically in the viewport */
}
.record-button {
background-color: rgb(255, 100, 100);
border: none;
color: white;
padding: 10px 20px;
border-radius: 50%; /* This creates the round shape */
cursor: pointer;
outline: none;
font-size: 16px;
width: 80px;
height: 80px;
/* Optional: Add some transition for interactions */
transition: background-color 0.3s;
}
.record-button:active {
background-color: darkred;
}
</style>
在 Python 端,我们需要创建一个 Streamlit 应用,用于接收和处理前端传递的 base64 编码数据。以下是 Python 端的代码示例:
import base64
import os
import streamlit.components.v1 as components
_RELEASE = True
if not _RELEASE:
_component_func = components.declare_component(
"record_button",
url="http://localhost:5173",
)
else:
parent_dir = os.path.dirname(os.path.abspath(__file__))
build_dir = os.path.join(parent_dir, "frontend/dist")
_component_func = components.declare_component(
"record_button",
path=build_dir
)
def record_button(size, key=None):
component_value = _component_func(size=size, key=key, default=None)
if component_value:
component_value = base64.b64decode(component_value)
return component_value
_RELEASE
参数,调试时可以设为 False
,使用本地 npm run dev
来调试。发布时先运行 npm run build
打包 frontend,然后将_RELEASE
设为 True
tsconfig.json
中加上 "compilerOptions.baseUrl": "."
,以及在 vite.config.ts
中加上 `base: './',不然路径会有问题,参考 how-to-view-a-component-in-release-mode-using-vite最后,我们将这个 Streamlit 组件打包并发布到 PyPI,以便其他用户可以轻松地安装和使用。打包和发布的过程涉及到一些配置和命令,具体步骤可以参考 Streamlit 官方文档(Streamlit Document - Publish a Component)和 Python 打包用户指南(Python Packaging User Guide)。
在发布之前,确保修改 MANIFEST.in 文件,将前端构建文件夹包含在内。同时,根据你的组件信息修改 pyproject.toml 文件。
python3 -m build # 打包
python3 -m twine upload --repository testpypi dist/* # 上传
注意文件夹名称一致,否则 pip 安装成功后 import 时也会出现 Module Name Not Found的错误
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。