generated from template/astro-simple-template
update
This commit is contained in:
@@ -7,7 +7,7 @@ import tailwindcss from '@tailwindcss/vite';
|
|||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
let target = process.env.VITE_API_URL || 'https://localhost:51015';
|
let target = process.env.VITE_API_URL || 'http://localhost:4005';
|
||||||
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
||||||
let proxy = {
|
let proxy = {
|
||||||
'/root/': {
|
'/root/': {
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/astro-simplate-template",
|
"name": "@kevisual/aura-center",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"basename": "/root/astro-simplate-template",
|
"basename": "/root/aura-center",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"pub": "envision deploy ./dist -k astro-simplate-template -v 0.0.1 -u",
|
"pub": "envision deploy ./dist -k aura-center -v 0.0.1 -u",
|
||||||
"sn": "pnpm dlx shadcn@latest add "
|
"sn": "pnpm dlx shadcn@latest add "
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"wavesurfer.js": "^7.11.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -48,5 +49,10 @@
|
|||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.18.3"
|
"packageManager": "pnpm@10.18.3",
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@tailwindcss/oxide",
|
||||||
|
"esbuild",
|
||||||
|
"sharp"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -62,6 +62,9 @@ importers:
|
|||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
|
wavesurfer.js:
|
||||||
|
specifier: ^7.11.0
|
||||||
|
version: 7.11.0
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^5.0.8
|
specifier: ^5.0.8
|
||||||
version: 5.0.8(@types/react@19.2.2)(react@19.2.0)
|
version: 5.0.8(@types/react@19.2.2)(react@19.2.0)
|
||||||
@@ -2273,6 +2276,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
wavesurfer.js@7.11.0:
|
||||||
|
resolution: {integrity: sha512-LOGdIBIKv/roYuQYClhoqhwbIdQL1GfobLnS2vx0heoLD9lu57OUHWE2DIsCNXBvCsmmbkUvJq9W8bPLPbikGw==}
|
||||||
|
|
||||||
web-namespaces@2.0.1:
|
web-namespaces@2.0.1:
|
||||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||||
|
|
||||||
@@ -4918,6 +4924,8 @@ snapshots:
|
|||||||
|
|
||||||
void-elements@3.1.0: {}
|
void-elements@3.1.0: {}
|
||||||
|
|
||||||
|
wavesurfer.js@7.11.0: {}
|
||||||
|
|
||||||
web-namespaces@2.0.1: {}
|
web-namespaces@2.0.1: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|||||||
3
src/apps/record/Wavesurfer.tsx
Normal file
3
src/apps/record/Wavesurfer.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const Wavesurfer = () => {
|
||||||
|
return <div>Wavesurfer</div>;
|
||||||
|
}
|
||||||
99
src/apps/record/components/Wavesurfer.tsx
Normal file
99
src/apps/record/components/Wavesurfer.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import WaveSurfer from "wavesurfer.js";
|
||||||
|
|
||||||
|
interface WavesurferProps {
|
||||||
|
audioUrl?: string;
|
||||||
|
isPlaying?: boolean;
|
||||||
|
onPlayStateChange?: (isPlaying: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Wavesurfer = ({
|
||||||
|
audioUrl,
|
||||||
|
isPlaying = false,
|
||||||
|
onPlayStateChange
|
||||||
|
}: WavesurferProps) => {
|
||||||
|
const waveformRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const wavesurferRef = useRef<WaveSurfer | null>(null);
|
||||||
|
|
||||||
|
// 播放模式的波形
|
||||||
|
useEffect(() => {
|
||||||
|
if (audioUrl && waveformRef.current && !wavesurferRef.current) {
|
||||||
|
wavesurferRef.current = WaveSurfer.create({
|
||||||
|
container: waveformRef.current,
|
||||||
|
waveColor: '#10b981',
|
||||||
|
progressColor: '#059669',
|
||||||
|
cursorColor: '#047857',
|
||||||
|
barWidth: 2,
|
||||||
|
barRadius: 3,
|
||||||
|
height: 60,
|
||||||
|
normalize: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
wavesurferRef.current.load(audioUrl);
|
||||||
|
|
||||||
|
// 监听播放状态
|
||||||
|
wavesurferRef.current.on('play', () => onPlayStateChange?.(true));
|
||||||
|
wavesurferRef.current.on('pause', () => onPlayStateChange?.(false));
|
||||||
|
wavesurferRef.current.on('finish', () => onPlayStateChange?.(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (wavesurferRef.current) {
|
||||||
|
wavesurferRef.current.destroy();
|
||||||
|
wavesurferRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [audioUrl, onPlayStateChange]);
|
||||||
|
|
||||||
|
// 外部播放控制
|
||||||
|
useEffect(() => {
|
||||||
|
if (wavesurferRef.current) {
|
||||||
|
if (isPlaying) {
|
||||||
|
wavesurferRef.current.play();
|
||||||
|
} else {
|
||||||
|
wavesurferRef.current.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isPlaying]);
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (wavesurferRef.current) {
|
||||||
|
wavesurferRef.current.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 暴露播放控制方法
|
||||||
|
const togglePlayback = () => {
|
||||||
|
if (wavesurferRef.current) {
|
||||||
|
if (wavesurferRef.current.isPlaying()) {
|
||||||
|
wavesurferRef.current.pause();
|
||||||
|
} else {
|
||||||
|
wavesurferRef.current.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 只在有音频URL时显示
|
||||||
|
if (!audioUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="bg-white rounded-lg p-3 border">
|
||||||
|
<div ref={waveformRef} className="w-full"></div>
|
||||||
|
<div className="flex items-center justify-center mt-2 text-sm text-gray-500">
|
||||||
|
点击波形或使用播放按钮控制播放
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露控制方法的接口
|
||||||
|
export interface WavesurferRef {
|
||||||
|
togglePlayback: () => void;
|
||||||
|
}
|
||||||
295
src/apps/record/index.tsx
Normal file
295
src/apps/record/index.tsx
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import { useState, useRef } from "react";
|
||||||
|
import { getText } from "./modules/get-text";
|
||||||
|
import { Wavesurfer } from "./components/Wavesurfer";
|
||||||
|
|
||||||
|
interface TranscriptionResult {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RecordApp = () => {
|
||||||
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
|
const [recordingTime, setRecordingTime] = useState(0);
|
||||||
|
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
|
||||||
|
const [audioUrl, setAudioUrl] = useState<string>("");
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [showWaveform, setShowWaveform] = useState(false);
|
||||||
|
const [isTranscribing, setIsTranscribing] = useState(false);
|
||||||
|
const [transcriptions, setTranscriptions] = useState<TranscriptionResult[]>([]);
|
||||||
|
const [currentText, setCurrentText] = useState("");
|
||||||
|
|
||||||
|
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const intervalRef = useRef<any | null>(null);
|
||||||
|
|
||||||
|
const startRecording = async () => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
const mediaRecorder = new MediaRecorder(stream);
|
||||||
|
mediaRecorderRef.current = mediaRecorder;
|
||||||
|
|
||||||
|
const chunks: BlobPart[] = [];
|
||||||
|
|
||||||
|
mediaRecorder.ondataavailable = (event) => {
|
||||||
|
if (event.data.size > 0) {
|
||||||
|
chunks.push(event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.onstop = () => {
|
||||||
|
const blob = new Blob(chunks, { type: "audio/webm" });
|
||||||
|
setAudioBlob(blob);
|
||||||
|
setAudioUrl(URL.createObjectURL(blob));
|
||||||
|
stream.getTracks().forEach(track => track.stop());
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.start();
|
||||||
|
setIsRecording(true);
|
||||||
|
setRecordingTime(0);
|
||||||
|
|
||||||
|
// 开始计时
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
setRecordingTime(prev => prev + 1);
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("录音失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRecording = () => {
|
||||||
|
if (mediaRecorderRef.current && isRecording) {
|
||||||
|
mediaRecorderRef.current.stop();
|
||||||
|
setIsRecording(false);
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const playAudio = () => {
|
||||||
|
if (audioRef.current && audioUrl) {
|
||||||
|
if (isPlaying) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
setIsPlaying(false);
|
||||||
|
} else {
|
||||||
|
audioRef.current.play();
|
||||||
|
setIsPlaying(true);
|
||||||
|
setShowWaveform(true); // 开始播放时显示波形
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlayStateChange = (playing: boolean) => {
|
||||||
|
setIsPlaying(playing);
|
||||||
|
// 如果播放结束,隐藏波形
|
||||||
|
if (!playing) {
|
||||||
|
setShowWaveform(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const transcribeAudio = async () => {
|
||||||
|
if (!audioBlob) return;
|
||||||
|
|
||||||
|
setIsTranscribing(true);
|
||||||
|
try {
|
||||||
|
// 将音频转换为base64
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = async () => {
|
||||||
|
const base64Audio = (reader.result as string).split(',')[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getText(base64Audio);
|
||||||
|
const text = typeof result === 'string' ? result : (result as any)?.data || '转录失败';
|
||||||
|
setCurrentText(text);
|
||||||
|
|
||||||
|
// 添加到转录历史
|
||||||
|
const newTranscription: TranscriptionResult = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
text,
|
||||||
|
timestamp: new Date()
|
||||||
|
};
|
||||||
|
setTranscriptions(prev => [newTranscription, ...prev]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("转录失败:", error);
|
||||||
|
setCurrentText("转录失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setIsTranscribing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(audioBlob);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("处理音频失败:", error);
|
||||||
|
setIsTranscribing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyText = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshTranscriptions = () => {
|
||||||
|
setTranscriptions([]);
|
||||||
|
setCurrentText("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-lg">
|
||||||
|
{/* 标题 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800 text-center">语音录制与转文字</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 录音控制区域 */}
|
||||||
|
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div className="flex items-center justify-center gap-4 mb-4">
|
||||||
|
{/* 录音/停止按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={isRecording ? stopRecording : startRecording}
|
||||||
|
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
|
||||||
|
isRecording
|
||||||
|
? "bg-red-500 hover:bg-red-600 text-white"
|
||||||
|
: "bg-blue-500 hover:bg-blue-600 text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isRecording ? "停止录音" : "开始录音"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 播放按钮 - 录制时隐藏 */}
|
||||||
|
{!isRecording && (
|
||||||
|
<button
|
||||||
|
onClick={playAudio}
|
||||||
|
disabled={!audioUrl}
|
||||||
|
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
|
||||||
|
audioUrl
|
||||||
|
? "bg-green-500 hover:bg-green-600 text-white"
|
||||||
|
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isPlaying ? "暂停播放" : "播放录音"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 转文字按钮 - 录制时隐藏 */}
|
||||||
|
{!isRecording && audioUrl && (
|
||||||
|
<button
|
||||||
|
onClick={transcribeAudio}
|
||||||
|
disabled={isTranscribing}
|
||||||
|
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
|
||||||
|
isTranscribing
|
||||||
|
? "bg-gray-400 text-white cursor-not-allowed"
|
||||||
|
: "bg-purple-500 hover:bg-purple-600 text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isTranscribing ? "转录中..." : "转文字"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 录音状态显示 */}
|
||||||
|
{isRecording && (
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<div className="flex items-center justify-center gap-2 text-red-600">
|
||||||
|
<div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||||
|
<span className="font-medium">录音中: {formatTime(recordingTime)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 播放波形显示 - 只在需要显示且有音频时显示 */}
|
||||||
|
{showWaveform && audioUrl && (
|
||||||
|
<Wavesurfer
|
||||||
|
audioUrl={audioUrl}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
onPlayStateChange={handlePlayStateChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 隐藏的音频元素 */}
|
||||||
|
{audioUrl && (
|
||||||
|
<audio
|
||||||
|
ref={audioRef}
|
||||||
|
src={audioUrl}
|
||||||
|
onEnded={() => setIsPlaying(false)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 转录结果区域 */}
|
||||||
|
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-700">最近转录</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => currentText && copyText(currentText)}
|
||||||
|
disabled={!currentText}
|
||||||
|
className={`px-3 py-1 rounded text-sm transition-colors ${
|
||||||
|
currentText
|
||||||
|
? "bg-blue-100 hover:bg-blue-200 text-blue-700"
|
||||||
|
: "bg-gray-200 text-gray-400 cursor-not-allowed"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={refreshTranscriptions}
|
||||||
|
className="px-3 py-1 rounded text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 当前转录文本 */}
|
||||||
|
<div className="min-h-[100px] p-4 bg-white border rounded-lg">
|
||||||
|
{isTranscribing ? (
|
||||||
|
<div className="flex items-center justify-center text-gray-500">
|
||||||
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-purple-500 mr-2"></div>
|
||||||
|
正在转录音频...
|
||||||
|
</div>
|
||||||
|
) : currentText ? (
|
||||||
|
<p className="text-gray-800 leading-relaxed">{currentText}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-400 text-center">转录结果将显示在这里</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 历史转录记录 */}
|
||||||
|
{transcriptions.length > 0 && (
|
||||||
|
<div className="p-4 bg-gray-50 rounded-lg">
|
||||||
|
<h3 className="text-md font-semibold text-gray-700 mb-3">历史记录</h3>
|
||||||
|
<div className="space-y-3 max-h-60 overflow-y-auto">
|
||||||
|
{transcriptions.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="p-3 bg-white border rounded-lg flex justify-between items-start"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-gray-800 text-sm mb-1">{item.text}</p>
|
||||||
|
<p className="text-gray-400 text-xs">
|
||||||
|
{item.timestamp.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => copyText(item.text)}
|
||||||
|
className="ml-3 px-2 py-1 text-xs bg-blue-100 hover:bg-blue-200 text-blue-700 rounded transition-colors"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
38
src/apps/record/modules/get-text.ts
Normal file
38
src/apps/record/modules/get-text.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { query } from '../../../modules/query'
|
||||||
|
|
||||||
|
export type AsrResponse = {
|
||||||
|
audio_info: {
|
||||||
|
/**
|
||||||
|
* 音频时长,单位为 ms
|
||||||
|
*/
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
result: {
|
||||||
|
additions: {
|
||||||
|
duration: string;
|
||||||
|
};
|
||||||
|
text: string;
|
||||||
|
utterances: Array<{
|
||||||
|
end_time: number;
|
||||||
|
start_time: number;
|
||||||
|
text: string;
|
||||||
|
words: Array<{
|
||||||
|
confidence: number;
|
||||||
|
end_time: number;
|
||||||
|
start_time: number;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const getText = async (base64Audio: string) => {
|
||||||
|
const res = await query.post<AsrResponse>({
|
||||||
|
path: 'asr',
|
||||||
|
key: 'text',
|
||||||
|
base64Audio
|
||||||
|
})
|
||||||
|
if (res.code !== 200) {
|
||||||
|
return '转录失败'
|
||||||
|
}
|
||||||
|
return res.data?.result?.text || '转录失败'
|
||||||
|
}
|
||||||
3
src/modules/query.ts
Normal file
3
src/modules/query.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Query } from '@kevisual/query';
|
||||||
|
|
||||||
|
export const query = new Query();
|
||||||
17
src/pages/record.astro
Normal file
17
src/pages/record.astro
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import { RecordApp } from '../apps/record/index';
|
||||||
|
import '../styles/global.css';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>语音录制与转文字</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="min-h-screen bg-gray-100 py-8">
|
||||||
|
<RecordApp client:load />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user