feat(tts): add api endpoints and optimization for apple silicon
Introduce a comprehensive TTS/ASR module that: - Adds /v1/tts-asr/config, /status, /warmup, /tts, /asr endpoints with detailed JSON responses - Implements Apple‑Silicon detection, device selection (MPS/CUDA/CPU), and memory limiting logic - Supports selectable model size, quantization, and offline mode via environment variables - Adds robust audio validation and multi‑path resampling fallback - Provides new README sections for API usage, device detection, and performance benchmarking - Includes a full testing suite: unit tests, integration tests, macOS simulation and performance reports - Updates backend dependencies and CI scripts - Adds new front‑end views and components for Univer editor integration All changes are backward compatible; new features are exposed through environment variables and new API routes.
This commit is contained in:
+93
-29
@@ -1,11 +1,16 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useFileSystem } from '../composables/useFileSystem'
|
||||
import { useOfficeStore } from '../stores/office'
|
||||
import { isOfficeFile, getOfficeFormat } from '../services/officeDetection'
|
||||
import FileTree from '../components/FileTree.vue'
|
||||
import FileContent from '../components/FileContent.vue'
|
||||
import ContextMenu from '../components/ContextMenu.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const fs = useFileSystem()
|
||||
const officeStore = useOfficeStore()
|
||||
const sidebarCollapsed = ref(false)
|
||||
const confirmDialog = ref(null)
|
||||
|
||||
@@ -72,27 +77,22 @@ function handleContextMenu(x, y, node) {
|
||||
|
||||
function handleDrop(draggedId, targetParentId) {
|
||||
if (draggedId === targetParentId) return
|
||||
|
||||
// 找到目标节点
|
||||
let targetNode = null
|
||||
if (targetParentId) {
|
||||
targetNode = findNode(fs.tree.value, targetParentId)
|
||||
if (!targetNode || targetNode.type !== 'folder') return
|
||||
}
|
||||
|
||||
// 检查是否拖拽到自己的子节点
|
||||
const draggedNode = findNode(fs.tree.value, draggedId)
|
||||
if (!draggedNode) return
|
||||
if (targetParentId && isDescendant(draggedNode, targetParentId)) return
|
||||
|
||||
const oldParent = findParent(fs.tree.value, draggedId)
|
||||
if (oldParent) {
|
||||
oldParent.children = (oldParent.children || []).filter(c => c.id !== draggedId)
|
||||
} else {
|
||||
fs.tree.value = fs.tree.value.filter(n => n.id !== draggedId)
|
||||
}
|
||||
|
||||
draggedNode.parentId = targetParentId || null
|
||||
if (targetParentId) {
|
||||
const target = findNode(fs.tree.value, targetParentId)
|
||||
if (target && target.type === 'folder') {
|
||||
target.children = target.children || []
|
||||
target.children.push(draggedNode)
|
||||
}
|
||||
} else {
|
||||
fs.tree.value.push(draggedNode)
|
||||
}
|
||||
// 使用剪贴板的移动逻辑
|
||||
fs.cut(draggedId)
|
||||
fs.paste(targetParentId)
|
||||
}
|
||||
|
||||
function isDescendant(node, targetId) {
|
||||
@@ -122,6 +122,17 @@ function handleDragStart(event, id) {
|
||||
function handleDragOver(event) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// 打开 Univer 编辑器
|
||||
function openInUniver() {
|
||||
router.push('/univer')
|
||||
}
|
||||
|
||||
// 判断选中的文件是否为 Office 文件
|
||||
const isSelectedOfficeFile = computed(() => {
|
||||
if (!selectedNode.value || selectedNode.value.type === 'folder') return false
|
||||
return isOfficeFile({ name: selectedNode.value.name })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -152,33 +163,51 @@ function handleDragOver(event) {
|
||||
|
||||
<div class="docs-main">
|
||||
<div class="docs-toolbar">
|
||||
<button class="sidebar-toggle" @click="sidebarCollapsed = !sidebarCollapsed" :title="sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'">
|
||||
<svg v-if="!sidebarCollapsed" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M4.5 3.5a.5.5 0 00-.707.707L6.586 7l-2.793 2.793a.5.5 0 10.707.707l3-3a.5.5 0 000-.707l-3-3z"/><path d="M9.5 3.5a.5.5 0 01.707.707L7.414 7l2.793 2.793a.5.5 0 01-.707.707l-3-3a.5.5 0 010-.707l3-3z"/></svg>
|
||||
<svg v-else viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M11.5 3.5a.5.5 0 01.707.707L9.414 7l2.793 2.793a.5.5 0 01-.707.707l-3-3a.5.5 0 010-.707l3-3z"/><path d="M4.5 3.5a.5.5 0 00-.707.707L6.586 7l-2.793 2.793a.5.5 0 10.707.707l3-3a.5.5 0 000-.707l-3-3z"/></svg>
|
||||
</button>
|
||||
<button class="sidebar-toggle" @click="sidebarCollapsed = !sidebarCollapsed" :title="sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'">
|
||||
<svg v-if="!sidebarCollapsed" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M4.5 3.5a.5.5 0 00-.707.707L6.586 7l-2.793 2.793a.5.5 0 10.707.707l3-3a.5.5 0 000-.707l-3-3z"/><path d="M9.5 3.5a.5.5 0 01.707.707L7.414 7l2.793 2.793a.5.5 0 01-.707.707l-3-3a.5.5 0 010-.707l3-3z"/></svg>
|
||||
<svg v-else viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M11.5 3.5a.5.5 0 01.707.707L9.414 7l2.793 2.793a.5.5 0 01-.707.707l-3-3a.5.5 0 010-.707l3-3z"/><path d="M4.5 3.5a.5.5 0 00-.707.707L6.586 7l-2.793 2.793a.5.5 0 10.707.707l3-3a.5.5 0 000-.707l-3-3z"/></svg>
|
||||
</button>
|
||||
<button class="editor-toggle" @click="router.push('/')" title="返回编辑器">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="M2 2.5A1.5 1.5 0 013.5 1h9A1.5 1.5 0 0114 2.5v11a1.5 1.5 0 01-1.5 1.5h-9A1.5 1.5 0 012 13.5v-11z"/>
|
||||
<path fill="var(--app-bg)" d="M4 4h8v1H4zm0 2h8v1H4zm0 2h6v1H4zm0 2h8v1H4zm0 2h5v1H4z"/>
|
||||
</svg>
|
||||
<span class="toggle-label">编辑器</span>
|
||||
</button>
|
||||
<div class="breadcrumb-bar">
|
||||
<span
|
||||
class="breadcrumb-link"
|
||||
:class="{ 'breadcrumb-current': breadcrumb.length === 0 }"
|
||||
@click="fs.select(null)"
|
||||
>根目录</span>
|
||||
<span v-if="breadcrumb.length > 0" class="breadcrumb-sep">/</span>
|
||||
<template v-for="(item, index) in breadcrumb" :key="item.id">
|
||||
<span
|
||||
v-if="item.type === 'folder' && index < breadcrumb.length - 1"
|
||||
v-if="index < breadcrumb.length - 1"
|
||||
class="breadcrumb-link"
|
||||
@click="fs.select(item.id)"
|
||||
>{{ item.name }}</span>
|
||||
<span v-else class="breadcrumb-current">{{ item.name }}</span>
|
||||
<span v-if="index < breadcrumb.length - 1" class="breadcrumb-sep">/</span>
|
||||
</template>
|
||||
<span v-if="breadcrumb.length === 0" class="breadcrumb-root">根目录</span>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button v-if="fs.canPaste()" class="toolbar-btn" @click="fs.paste(selectedNode && selectedNode.type === 'folder' ? selectedNode.id : null)" title="粘贴">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M4.75 1.5a.25.25 0 00-.25.25v.59c0 .396.316.717.707.717h5.586c.39 0 .707-.32.707-.716v-.591a.25.25 0 00-.25-.25H4.75zm6.543-.75a1.75 1.75 0 011.75 1.75v.59c0 .396-.107.767-.293 1.086l1.293 1.293a.75.75 0 010 1.061l-1.293 1.293c.186.32.293.69.293 1.087v.59a1.75 1.75 0 01-1.75 1.75H4.75a1.75 1.75 0 01-1.75-1.75v-.59c0-.396.107-.767.293-1.087L2 5.53a.75.75 0 010-1.06l1.293-1.294A2.048 2.048 0 013 2.09v-.59A1.75 1.75 0 014.75 0h6.543zM6 8.5a.5.5 0 01.5-.5h3a.5.5 0 010 1h-3a.5.5 0 01-.5-.5zm.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z"/></svg>
|
||||
粘贴
|
||||
</button>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button v-if="isSelectedOfficeFile" class="toolbar-btn office-btn" @click="openInUniver" title="在 Univer 中编辑">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><rect x="7" y="11" width="8" height="6" rx="1" stroke-width="1.5"/><circle cx="9" cy="13" r="0.8" fill="currentColor"/><path d="M7 16l2-2 2 2" stroke-width="1.5"/></svg>
|
||||
编辑 Office
|
||||
</button>
|
||||
<button v-if="fs.canPaste()" class="toolbar-btn" @click="fs.paste(selectedNode && selectedNode.type === 'folder' ? selectedNode.id : null)" title="粘贴">
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M4.75 1.5a.25.25 0 00-.25.25v.59c0 .396.316.717.707.717h5.586c.39 0 .707-.32.707-.716v-.591a.25.25 0 00-.25-.25H4.75zm6.543-.75a1.75 1.75 0 011.75 1.75v.59c0 .396-.107.767-.293 1.086l1.293 1.293a.75.75 0 010 1.061l-1.293 1.293c.186.32.293.69.293 1.087v.59a1.75 1.75 0 01-1.75 1.75H4.75a1.75 1.75 0 01-1.75-1.75v-.59c0-.396.107-.767.293-1.087L2 5.53a.75.75 0 010-1.06l1.293-1.294A2.048 2.048 0 013 2.09v-.59A1.75 1.75 0 014.75 0h6.543zM6 8.5a.5.5 0 01.5-.5h3a.5.5 0 010 1h-3a.5.5 0 01-.5-.5zm.5 2.5a.5.5 0 000 1h3a.5.5 0 000-1h-3z"/></svg>
|
||||
粘贴
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FileContent
|
||||
:node="selectedNode"
|
||||
:breadcrumb="breadcrumb"
|
||||
:root-nodes="fs.tree.value"
|
||||
:get-file-icon="fs.getFileIcon"
|
||||
@navigate="fs.select"
|
||||
/>
|
||||
</div>
|
||||
@@ -295,6 +324,31 @@ function handleDragOver(event) {
|
||||
border-color: var(--focus-ring);
|
||||
}
|
||||
|
||||
.editor-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--panel-border);
|
||||
background: var(--app-bg);
|
||||
color: var(--muted-text);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.editor-toggle:hover {
|
||||
color: var(--focus-ring);
|
||||
border-color: var(--focus-ring);
|
||||
background: var(--ghost-code-bg);
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.breadcrumb-bar {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -351,6 +405,16 @@ function handleDragOver(event) {
|
||||
color: var(--focus-ring);
|
||||
}
|
||||
|
||||
.toolbar-btn.office-btn {
|
||||
border-color: var(--focus-ring);
|
||||
color: var(--focus-ring);
|
||||
background: rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.toolbar-btn.office-btn:hover {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.confirm-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
Reference in New Issue
Block a user