新增分页,新增win打包

This commit is contained in:
kura 2026-05-01 22:31:32 +08:00
parent 15ad4a1f07
commit cbe5955c6e
10 changed files with 604 additions and 649 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
/src-tauri/target/
**/*.rs.bk
/src-tauri/model/
/src-tauri/vendor/ffmpeg/

View File

@ -10,9 +10,11 @@
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"prepare-ffmpeg-macos": "sh ./scripts/prepare-bundled-ffmpeg.sh",
"prepare-ffmpeg-windows": "powershell -ExecutionPolicy Bypass -File ./scripts/prepare-ffmpeg-windows.ps1",
"prepare-licenses-macos": "python3 ./scripts/prepare-bundled-licenses.py",
"tauri-build-app": "npm run prepare-ffmpeg-macos && npm run prepare-licenses-macos && tauri build --bundles app",
"tauri-build-dmg": "sh ./scripts/build-macos-dmg.sh"
"tauri-build-dmg": "sh ./scripts/build-macos-dmg.sh",
"tauri-build-windows": "tauri build --bundles nsis"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",

View File

@ -0,0 +1,83 @@
param(
[string]$FFmpegSource = ""
)
$ErrorActionPreference = "Stop"
$ROOT_DIR = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
$VENDOR_DIR = Join-Path $ROOT_DIR "src-tauri\vendor\ffmpeg\windows-x86_64"
$BIN_DIR = Join-Path $VENDOR_DIR "bin"
$FFMPEG_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z"
# Ensure target directory exists
if (-not (Test-Path $BIN_DIR)) {
New-Item -ItemType Directory -Path $BIN_DIR -Force | Out-Null
}
# Strategy 1: if -FFmpegSource explicitly provided, use it
if ($FFmpegSource) {
if (-not (Test-Path $FFmpegSource)) {
Write-Host "ffmpeg not found at: $FFmpegSource" -ForegroundColor Red
exit 1
}
Write-Host "Copying ffmpeg from: $FFmpegSource"
Copy-Item -Path $FFmpegSource -Destination (Join-Path $BIN_DIR "ffmpeg.exe") -Force
Write-Host "ffmpeg bundled at: $BIN_DIR\ffmpeg.exe" -ForegroundColor Green
exit 0
}
# Strategy 2: download Gyan Essentials build via 7-Zip
$sevenZipPaths = @(
"${env:ProgramFiles}\7-Zip\7z.exe",
"${env:ProgramFiles(x86)}\7-Zip\7z.exe",
"${env:LOCALAPPDATA}\Programs\7-Zip\7z.exe"
)
$sevenZip = $sevenZipPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($sevenZip) {
$tempDir = Join-Path $env:TEMP "ffmpeg-essentials"
$archivePath = Join-Path $tempDir "ffmpeg.7z"
try {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
Write-Host "Downloading ffmpeg essentials from: $FFMPEG_URL" -ForegroundColor Cyan
Invoke-WebRequest -Uri $FFMPEG_URL -OutFile $archivePath -UseBasicParsing
Write-Host "Extracting..." -ForegroundColor Cyan
& $sevenZip x $archivePath -o"$tempDir\out" -y -bso0 | Out-Null
$extractedExe = Get-ChildItem -Path "$tempDir\out" -Recurse -Filter "ffmpeg.exe" | Select-Object -First 1 -ExpandProperty FullName
if (-not $extractedExe) {
throw "ffmpeg.exe not found in extracted archive"
}
Copy-Item -Path $extractedExe -Destination (Join-Path $BIN_DIR "ffmpeg.exe") -Force
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
$size = (Get-Item (Join-Path $BIN_DIR "ffmpeg.exe")).Length / 1MB
Write-Host "ffmpeg essentials bundled at: $BIN_DIR\ffmpeg.exe ($([math]::Round($size)) MB)" -ForegroundColor Green
exit 0
}
catch {
Write-Host "Download/extraction failed: $_" -ForegroundColor Yellow
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
else {
Write-Host "7-Zip not found, recommend installing it for automatic download." -ForegroundColor Yellow
Write-Host "Download manually from: $FFMPEG_URL and extract ffmpeg.exe to $BIN_DIR" -ForegroundColor Yellow
}
# Strategy 3: fall back to copying system ffmpeg
$systemFFmpeg = (Get-Command ffmpeg -ErrorAction SilentlyContinue).Source
if ($systemFFmpeg) {
Write-Host "Falling back to system ffmpeg: $systemFFmpeg" -ForegroundColor Cyan
Copy-Item -Path $systemFFmpeg -Destination (Join-Path $BIN_DIR "ffmpeg.exe") -Force
$size = (Get-Item (Join-Path $BIN_DIR "ffmpeg.exe")).Length / 1MB
Write-Host "ffmpeg bundled at: $BIN_DIR\ffmpeg.exe ($([math]::Round($size)) MB)" -ForegroundColor Green
exit 0
}
Write-Host "ERROR: No ffmpeg found. Install ffmpeg or specify path via -FFmpegSource." -ForegroundColor Red
exit 1

View File

@ -109,6 +109,41 @@ impl WhisperEngine {
|| (total_seconds > 60.0 && vad_text_len < (total_seconds / 3.0) as usize));
if should_retry_full_audio {
// 如果 VAD 覆盖率足够高且仅尾部缺失,只转录尾部而非全音频
// 避免数小时的长音频被重新处理一遍
let is_tail_only = !segments.is_empty()
&& vad_coverage >= 0.60
&& vad_end + 5.0 < total_seconds
&& vad_text_len >= (total_seconds / 3.0) as usize;
if is_tail_only {
let tail_start = vad_end.max(0.0);
on_log(format!(
"whisper: VAD tail incomplete, retrying tail ({:.2}s-{:.2}s, existing_segments={})",
tail_start,
total_seconds,
segments.len()
))?;
let tail_segments = transcribe_clip(
&mut state,
&audio,
normalized_ranges.len(),
tail_start,
total_seconds,
task_id,
detected_language,
target_lang,
should_translate,
segments.len(),
&mut on_segment,
&mut on_log,
)?;
segments.extend(tail_segments);
on_log(format!(
"whisper: total segments after tail retry={}",
segments.len()
))?;
} else {
on_log(format!(
"whisper: VAD result looks incomplete, retrying full audio (segments={}, chars={}, end={:.2}s/{:.2}s, coverage={:.1}%)",
segments.len(),
@ -150,6 +185,7 @@ impl WhisperEngine {
segments.iter().cloned().try_for_each(&mut on_segment)?;
}
}
}
on_log(format!("whisper: total emitted segments={}", segments.len()))?;
Ok(segments)

View File

@ -2,6 +2,8 @@
import { computed, ref, nextTick, watch } from 'vue'
import type { SubtitleSegment, SubtitleTask } from '../lib/types'
const PAGE_SIZE = 200
const props = defineProps<{
task: SubtitleTask | null
logs: string[]
@ -38,6 +40,33 @@ const sortedSegments = computed(() =>
}),
)
const currentPage = ref(1)
const totalPages = computed(() => Math.max(1, Math.ceil(sortedSegments.value.length / PAGE_SIZE)))
const hasPagination = computed(() => sortedSegments.value.length > PAGE_SIZE)
const visibleSegments = computed(() => {
const start = (currentPage.value - 1) * PAGE_SIZE
return sortedSegments.value.slice(start, start + PAGE_SIZE)
})
watch(totalPages, (newTotal) => {
if (currentPage.value > newTotal) {
currentPage.value = newTotal
}
})
function goToPage(page: number) {
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
}
function prevPage() {
goToPage(currentPage.value - 1)
}
function nextPage() {
goToPage(currentPage.value + 1)
}
const logsExpanded = ref(false)
const logContainerRef = ref<HTMLElement | null>(null)
@ -98,8 +127,14 @@ function updateTranslatedText(segment: SubtitleSegment, value: string) {
</div>
<div v-else class="segment-list">
<div v-if="hasPagination" class="pagination-bar">
<button class="button small" :disabled="currentPage <= 1" @click="prevPage"></button>
<span class="pagination-info">{{ $t('editor.pageInfo', { current: currentPage, total: totalPages }) }}</span>
<button class="button small" :disabled="currentPage >= totalPages" @click="nextPage"></button>
<span class="pagination-count">{{ $t('editor.segments', { count: sortedSegments.length }) }}</span>
</div>
<article
v-for="segment in sortedSegments"
v-for="segment in visibleSegments"
:key="segment.id"
class="segment-item"
>
@ -116,6 +151,11 @@ function updateTranslatedText(segment: SubtitleSegment, value: string) {
@change="updateTranslatedText(segment, ($event.target as HTMLTextAreaElement).value)"
/>
</article>
<div v-if="hasPagination" class="pagination-bar bottom">
<button class="button small" :disabled="currentPage <= 1" @click="prevPage"></button>
<span class="pagination-info">{{ $t('editor.pageInfo', { current: currentPage, total: totalPages }) }}</span>
<button class="button small" :disabled="currentPage >= totalPages" @click="nextPage"></button>
</div>
</div>
<div class="log-drawer" :class="{ expanded: logsExpanded }">

View File

@ -55,6 +55,7 @@ function isActive(task: SubtitleTask): boolean {
v-for="task in tasks"
:key="task.id"
class="task-item-wrapper"
:class="{ processing: isActive(task) }"
>
<button
class="task-item"

View File

@ -86,5 +86,6 @@ export default {
source: 'Source',
logs: 'Logs',
noLogs: 'No logs',
pageInfo: 'Page {current}/{total}',
},
}

View File

@ -86,5 +86,6 @@ export default {
source: '原文',
logs: '日志',
noLogs: '暂无日志',
pageInfo: '第 {current}/{total} 页',
},
}

View File

@ -443,6 +443,50 @@ textarea {
box-shadow: 0 0 6px rgba(45, 106, 79, 0.4);
}
@keyframes rotate-glow {
to {
transform: rotate(360deg);
}
}
.task-item-wrapper.processing {
position: relative;
overflow: hidden;
border-radius: var(--radius-md);
}
.task-item-wrapper.processing::before {
content: '';
position: absolute;
width: 150%;
height: 300%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
background: conic-gradient(
transparent 0deg,
#8a9bb5 90deg,
#d0d7e0 180deg,
transparent 270deg
);
animation: rotate-glow 2s linear infinite;
}
.task-item-wrapper.processing::after {
content: '';
position: absolute;
inset: 3px;
border-radius: calc(var(--radius-md) - 3px);
background: var(--c-bg);
z-index: 1;
}
.task-item-wrapper.processing .task-item {
position: relative;
z-index: 2;
}
.task-item.failed {
border-color: var(--c-error);
background: rgba(220, 38, 38, 0.03);
@ -789,6 +833,41 @@ textarea {
margin-bottom: 4px;
}
.pagination-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 12px 0;
position: sticky;
top: 0;
background: var(--c-surface-gradient);
border-bottom: 1px solid var(--c-border);
z-index: 1;
border-radius: var(--radius-md) var(--radius-md) 0 0;
}
.pagination-bar.bottom {
position: static;
border-bottom: none;
border-top: 1px solid var(--c-border);
border-radius: 0 0 var(--radius-md) var(--radius-md);
}
.pagination-info {
font-size: 12px;
color: var(--c-text-secondary);
font-weight: 500;
min-width: 100px;
text-align: center;
}
.pagination-count {
font-size: 11px;
color: var(--c-text-tertiary);
margin-left: auto;
}
@media (max-width: 1360px) {
.content-grid {
grid-template-columns: 280px minmax(0, 1fr);

941
yarn.lock

File diff suppressed because it is too large Load Diff