Compare commits

..

3 Commits

Author SHA1 Message Date
CI Bot 57d45c3219 fix: 恢复被ruff误删的向后兼容re-export常量
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 19s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 37s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 32s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 9s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m25s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m24s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 54s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 1m38s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m32s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m10s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m53s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 50m4s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 28s
- 9个engine模块的re-export常量被ruff --fix误删(F401)
- 这些是测试文件依赖的向后兼容导出
- 全部加回并标记 # noqa: F401
2026-07-27 12:05:09 +08:00
CI Bot fbf3c5288e fix: 全量修复ruff/black/isort代码质量问题
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 23s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 58s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 53s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m56s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m1s
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 1m35s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m43s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m6s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m2s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 3m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m59s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 2m58s
- 修复50个ruff错误(未使用import、重复定义、命名歧义等)
- 解决black与isort在长import行上的死循环问题
- 2个文件使用isort:off保护向后兼容re-export块
- 全量657个文件通过ruff+black+isort三重检查
2026-07-27 11:49:17 +08:00
CI Bot c8163cbadc fix: 修复全量ruff/black/isort代码质量问题 (50个错误)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 29s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 54s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 7s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m37s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m55s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m37s
AI Code Review / AI Code Review (pull_request) Successful in 1m43s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 26s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m4s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 3m35s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m44s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 7m16s
2026-07-27 11:25:58 +08:00
27 changed files with 275 additions and 419 deletions
-1
View File
@@ -12,7 +12,6 @@
from __future__ import annotations
import json
import logging
from typing import Any, Dict, List, Optional
@@ -2,27 +2,130 @@
* 查重结果列表页面 — V21 设计系统
* 胶囊筛选 + 卡片列表,零 antd 依赖
*/
import React from "react"
import { Button } from "@/components/ui"
import PageHead from "@/components/layout/PageHead"
import React, { useState, useMemo } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { Button, Tag, Tooltip } from "@/components/ui"
import { useNavigate } from "react-router-dom"
import {
getDuplicationRecords,
deleteDuplicationRecord,
retryDuplication,
type DuplicationStatus,
} from "@/api/duplication"
import "./duplication.css"
import useDuplicationResults from "./hooks/useDuplicationResults"
import FilterBar from "./components/FilterBar"
import EmptyState from "./components/EmptyState"
import ResultCard from "./components/ResultCard"
import PageHead from "@/components/layout/PageHead"
/** 风险等级分类 */
type RiskFilter = "all" | "high" | "medium" | "low"
/** 状态配置 */
const STATUS_CONFIG: Record<
DuplicationStatus,
{
variant: "primary" | "warning" | "success" | "error"
text: string
icon: string
}
> = {
pending: { variant: "primary", text: "等待中", icon: "⏳" },
processing: { variant: "warning", text: "查重中", icon: "🔄" },
completed: { variant: "success", text: "已完成", icon: "✅" },
failed: { variant: "error", text: "失败", icon: "❌" },
}
/** 根据查重率获取风险等级 */
const getRiskLevel = (rate?: number): "low" | "medium" | "high" => {
if (rate === undefined) return "low"
if (rate <= 10) return "low"
if (rate <= 30) return "medium"
return "high"
}
/** 风险等级标签 */
const RISK_LABELS: Record<string, string> = {
low: "低风险",
medium: "中风险",
high: "高风险",
}
/** 格式化文件大小 */
const formatSize = (bytes: number) => {
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
}
/** 格式化时长 */
const formatDuration = (seconds?: number) => {
if (!seconds) return "-"
const totalSec = Math.round(seconds)
const m = Math.floor(totalSec / 60)
const s = totalSec % 60
return m > 0 ? `${m}${s}` : `${s}`
}
/** 简易 toast */
interface ToastState {
message: string
type: "success" | "error" | "warning"
}
const FILTER_OPTIONS: { key: RiskFilter; label: string }[] = [
{ key: "all", label: "全部" },
{ key: "low", label: "低风险" },
{ key: "medium", label: "中风险" },
{ key: "high", label: "高风险" },
]
const DuplicationResults: React.FC = () => {
const {
isLoading,
filteredRecords,
riskFilter,
setRiskFilter,
toast,
handleDelete,
handleRetry,
handleView,
handleUpload,
} = useDuplicationResults()
const navigate = useNavigate()
const queryClient = useQueryClient()
const [riskFilter, setRiskFilter] = useState<RiskFilter>("all")
const [toast, setToast] = useState<ToastState | null>(null)
const showToast = (message: string, type: "success" | "error" | "warning") => {
setToast({ message, type })
setTimeout(() => setToast(null), 3000)
}
// 获取查重记录
const { data: records = [], isLoading } = useQuery({
queryKey: ["duplication-records"],
queryFn: getDuplicationRecords,
})
// 删除
const deleteMutation = useMutation({
mutationFn: deleteDuplicationRecord,
onSuccess: () => {
showToast("已删除", "success")
queryClient.invalidateQueries({ queryKey: ["duplication-records"] })
},
onError: () => {
showToast("删除失败", "error")
},
})
// 重新查重
const retryMutation = useMutation({
mutationFn: retryDuplication,
onSuccess: () => {
showToast("已重新提交查重", "success")
queryClient.invalidateQueries({ queryKey: ["duplication-records"] })
},
onError: () => {
showToast("重新查重失败", "error")
},
})
/** 按风险等级筛选 */
const filteredRecords = useMemo(() => {
if (riskFilter === "all") return records
return records.filter((r) => {
if (r.status !== "completed") return riskFilter === "low"
return getRiskLevel(r.duplicate_rate) === riskFilter
})
}, [records, riskFilter])
return (
<div className="dup-page">
@@ -33,31 +136,136 @@ const DuplicationResults: React.FC = () => {
title="查重记录"
actions={
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<FilterBar value={riskFilter} onChange={setRiskFilter} />
<Button buttonType="primary" buttonSize="md" onClick={handleUpload}>
{/* 筛选胶囊 */}
<div className="dup-filter">
{FILTER_OPTIONS.map((opt) => (
<button
key={opt.key}
className={`dup-filter-btn ${riskFilter === opt.key ? "active" : ""}`}
onClick={() => setRiskFilter(opt.key)}
>
{opt.label}
</button>
))}
</div>
<Button
buttonType="primary"
buttonSize="md"
onClick={() => navigate("/app/duplication")}
>
📤
</Button>
</div>
}
/>
{/* 加载中 / 空状态 */}
{(isLoading || filteredRecords.length === 0) && (
<EmptyState isLoading={isLoading} riskFilter={riskFilter} />
{/* 加载中 */}
{isLoading && (
<div className="dup-results-empty">
<div className="dup-results-empty-icon"></div>
<p>...</p>
</div>
)}
{/* 空状态 */}
{!isLoading && filteredRecords.length === 0 && (
<div className="dup-results-empty">
<div className="dup-results-empty-icon">📭</div>
<p>
{riskFilter === "all"
? "暂无查重记录,上传视频开始查重吧"
: `没有${RISK_LABELS[riskFilter]}的记录`}
</p>
</div>
)}
{/* 结果卡片列表 */}
{!isLoading && filteredRecords.length > 0 && (
<div className="dup-results-list">
{filteredRecords.map((record) => (
<ResultCard
key={record.id}
record={record}
onView={handleView}
onDelete={handleDelete}
onRetry={handleRetry}
/>
))}
{filteredRecords.map((record) => {
const statusCfg = STATUS_CONFIG[record.status]
const riskLevel = getRiskLevel(record.duplicate_rate)
const rateValue = record.duplicate_rate
return (
<div
key={record.id}
className="dup-result-card"
onClick={() => {
if (record.status === "completed") {
navigate(`/duplication/${record.id}`)
}
}}
>
{/* 缩略图 */}
<div className="dup-result-card-thumb">🎬</div>
{/* 信息区 */}
<div className="dup-result-card-body">
<h4>{record.filename}</h4>
<div className="dup-result-card-meta">
<Tag variant={statusCfg.variant}>
{statusCfg.icon} {statusCfg.text}
</Tag>
<span>{formatSize(record.file_size)}</span>
<span>{formatDuration(record.duration_seconds)}</span>
<span>{new Date(record.created_at).toLocaleDateString("zh-CN")}</span>
{record.status === "completed" && record.duplicate_count !== undefined && (
<span>{record.duplicate_count} </span>
)}
</div>
</div>
{/* 查重率 */}
<div className="dup-result-card-score">
{record.status === "completed" && rateValue !== undefined ? (
<>
<div className="dup-score-bar">
<div
className={`dup-score-bar-fill ${riskLevel}`}
style={{ width: `${Math.min(rateValue, 100)}%` }}
/>
</div>
<span className={`dup-score-value ${riskLevel}`}>
{rateValue.toFixed(1)}%
</span>
</>
) : record.status === "failed" ? (
<Tooltip title="重新查重">
<Button
buttonType="ghost"
buttonSize="sm"
onClick={(e) => {
e.stopPropagation()
retryMutation.mutate(record.id)
}}
>
🔄
</Button>
</Tooltip>
) : (
<span style={{ color: "var(--text-secondary)", fontSize: 12 }}>
{record.status === "processing" ? "分析中..." : "—"}
</span>
)}
</div>
{/* 删除按钮 */}
<Button
buttonType="ghost"
buttonSize="sm"
onClick={(e) => {
e.stopPropagation()
if (window.confirm("确定删除此记录?")) {
deleteMutation.mutate(record.id)
}
}}
>
🗑
</Button>
</div>
)
})}
</div>
)}
</div>
@@ -1,32 +0,0 @@
import React from "react"
import { RISK_LABELS } from "../constants"
import type { RiskFilter } from "../types"
interface EmptyStateProps {
isLoading: boolean
riskFilter: RiskFilter
}
const EmptyState: React.FC<EmptyStateProps> = ({ isLoading, riskFilter }) => {
if (isLoading) {
return (
<div className="dup-results-empty">
<div className="dup-results-empty-icon"></div>
<p>...</p>
</div>
)
}
return (
<div className="dup-results-empty">
<div className="dup-results-empty-icon">📭</div>
<p>
{riskFilter === "all"
? "暂无查重记录,上传视频开始查重吧"
: `没有${RISK_LABELS[riskFilter]}的记录`}
</p>
</div>
)
}
export default EmptyState
@@ -1,26 +0,0 @@
import React from "react"
import type { RiskFilter } from "../types"
import { FILTER_OPTIONS } from "../constants"
interface FilterBarProps {
value: RiskFilter
onChange: (value: RiskFilter) => void
}
const FilterBar: React.FC<FilterBarProps> = ({ value, onChange }) => {
return (
<div className="dup-filter">
{FILTER_OPTIONS.map((opt) => (
<button
key={opt.key}
className={`dup-filter-btn ${value === opt.key ? "active" : ""}`}
onClick={() => onChange(opt.key)}
>
{opt.label}
</button>
))}
</div>
)
}
export default FilterBar
@@ -1,95 +0,0 @@
import React from "react"
import { Button, Tag, Tooltip } from "@/components/ui"
import type { DuplicationRecord } from "@/api/duplication"
import { STATUS_CONFIG } from "../constants"
import { getRiskLevel, formatSize, formatDuration } from "../utils"
interface ResultCardProps {
record: DuplicationRecord
onView: (id: string) => void
onDelete: (id: string) => void
onRetry: (id: string) => void
}
const ResultCard: React.FC<ResultCardProps> = ({ record, onView, onDelete, onRetry }) => {
const statusCfg = STATUS_CONFIG[record.status]
const riskLevel = getRiskLevel(record.duplicate_rate)
const rateValue = record.duplicate_rate
const handleClick = () => {
if (record.status === "completed") {
onView(record.id)
}
}
return (
<div key={record.id} className="dup-result-card" onClick={handleClick}>
{/* 缩略图 */}
<div className="dup-result-card-thumb">🎬</div>
{/* 信息区 */}
<div className="dup-result-card-body">
<h4>{record.filename}</h4>
<div className="dup-result-card-meta">
<Tag variant={statusCfg.variant}>
{statusCfg.icon} {statusCfg.text}
</Tag>
<span>{formatSize(record.file_size)}</span>
<span>{formatDuration(record.duration_seconds)}</span>
<span>{new Date(record.created_at).toLocaleDateString("zh-CN")}</span>
{record.status === "completed" && record.duplicate_count !== undefined && (
<span>{record.duplicate_count} </span>
)}
</div>
</div>
{/* 查重率 */}
<div className="dup-result-card-score">
{record.status === "completed" && rateValue !== undefined ? (
<>
<div className="dup-score-bar">
<div
className={`dup-score-bar-fill ${riskLevel}`}
style={{ width: `${Math.min(rateValue, 100)}%` }}
/>
</div>
<span className={`dup-score-value ${riskLevel}`}>{rateValue.toFixed(1)}%</span>
</>
) : record.status === "failed" ? (
<Tooltip title="重新查重">
<Button
buttonType="ghost"
buttonSize="sm"
onClick={(e) => {
e.stopPropagation()
onRetry(record.id)
}}
>
🔄
</Button>
</Tooltip>
) : (
<span style={{ color: "var(--text-secondary)", fontSize: 12 }}>
{record.status === "processing" ? "分析中..." : "—"}
</span>
)}
</div>
{/* 删除按钮 */}
<Button
buttonType="ghost"
buttonSize="sm"
onClick={(e) => {
e.stopPropagation()
if (window.confirm("确定删除此记录?")) {
onDelete(record.id)
}
}}
>
🗑
</Button>
</div>
)
}
export default ResultCard
@@ -1,31 +0,0 @@
import type { DuplicationStatus } from "@/api/duplication"
import type { RiskFilter } from "./types"
/** 状态配置 */
export const STATUS_CONFIG: Record<
DuplicationStatus,
{
variant: "primary" | "warning" | "success" | "error"
text: string
icon: string
}
> = {
pending: { variant: "primary", text: "等待中", icon: "⏳" },
processing: { variant: "warning", text: "查重中", icon: "🔄" },
completed: { variant: "success", text: "已完成", icon: "✅" },
failed: { variant: "error", text: "失败", icon: "❌" },
}
/** 风险等级标签 */
export const RISK_LABELS: Record<string, string> = {
low: "低风险",
medium: "中风险",
high: "高风险",
}
export const FILTER_OPTIONS: { key: RiskFilter; label: string }[] = [
{ key: "all", label: "全部" },
{ key: "low", label: "低风险" },
{ key: "medium", label: "中风险" },
{ key: "high", label: "高风险" },
]
@@ -1,106 +0,0 @@
import { useState, useMemo } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { useNavigate } from "react-router-dom"
import {
getDuplicationRecords,
deleteDuplicationRecord,
retryDuplication,
type DuplicationRecord,
} from "@/api/duplication"
import type { RiskFilter, ToastState } from "../types"
import { getRiskLevel } from "../utils"
interface UseDuplicationResultsReturn {
records: DuplicationRecord[]
isLoading: boolean
filteredRecords: DuplicationRecord[]
riskFilter: RiskFilter
setRiskFilter: (filter: RiskFilter) => void
toast: ToastState | null
handleDelete: (id: string) => void
handleRetry: (id: string) => void
handleView: (id: string) => void
handleUpload: () => void
}
const useDuplicationResults = (): UseDuplicationResultsReturn => {
const navigate = useNavigate()
const queryClient = useQueryClient()
const [riskFilter, setRiskFilter] = useState<RiskFilter>("all")
const [toast, setToast] = useState<ToastState | null>(null)
const showToast = (message: string, type: "success" | "error" | "warning") => {
setToast({ message, type })
setTimeout(() => setToast(null), 3000)
}
// 获取查重记录
const { data: records = [], isLoading } = useQuery({
queryKey: ["duplication-records"],
queryFn: getDuplicationRecords,
})
// 删除
const deleteMutation = useMutation({
mutationFn: deleteDuplicationRecord,
onSuccess: () => {
showToast("已删除", "success")
queryClient.invalidateQueries({ queryKey: ["duplication-records"] })
},
onError: () => {
showToast("删除失败", "error")
},
})
// 重新查重
const retryMutation = useMutation({
mutationFn: retryDuplication,
onSuccess: () => {
showToast("已重新提交查重", "success")
queryClient.invalidateQueries({ queryKey: ["duplication-records"] })
},
onError: () => {
showToast("重新查重失败", "error")
},
})
/** 按风险等级筛选 */
const filteredRecords = useMemo(() => {
if (riskFilter === "all") return records
return records.filter((r) => {
if (r.status !== "completed") return riskFilter === "low"
return getRiskLevel(r.duplicate_rate) === riskFilter
})
}, [records, riskFilter])
const handleDelete = (id: string) => {
deleteMutation.mutate(id)
}
const handleRetry = (id: string) => {
retryMutation.mutate(id)
}
const handleView = (id: string) => {
navigate(`/duplication/${id}`)
}
const handleUpload = () => {
navigate("/app/duplication")
}
return {
records,
isLoading,
filteredRecords,
riskFilter,
setRiskFilter,
toast,
handleDelete,
handleRetry,
handleView,
handleUpload,
}
}
export default useDuplicationResults
-8
View File
@@ -1,8 +0,0 @@
/** 风险等级分类 */
export type RiskFilter = "all" | "high" | "medium" | "low"
/** 简易 toast */
export interface ToastState {
message: string
type: "success" | "error" | "warning"
}
-23
View File
@@ -1,23 +0,0 @@
/** 根据查重率获取风险等级 */
export const getRiskLevel = (rate?: number): "low" | "medium" | "high" => {
if (rate === undefined) return "low"
if (rate <= 10) return "low"
if (rate <= 30) return "medium"
return "high"
}
/** 格式化文件大小 */
export const formatSize = (bytes: number) => {
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
}
/** 格式化时长 */
export const formatDuration = (seconds?: number) => {
if (!seconds) return "-"
const totalSec = Math.round(seconds)
const m = Math.floor(totalSec / 60)
const s = totalSec % 60
return m > 0 ? `${m}${s}` : `${s}`
}
@@ -9,12 +9,11 @@
from __future__ import annotations
import logging
from typing import Optional
from packages.domain.chroma_key_config import CHROMA_KEY_PRESETS # noqa: F401
from packages.domain.chroma_key_config import apply_chroma_key_if_needed # noqa: F401
from packages.domain.chroma_key_config import (
CHROMA_KEY_PRESETS,
ChromaKeyConfig,
apply_chroma_key_if_needed,
)
from packages.domain.chroma_key_config import ( # noqa: F401 — 向后兼容
build_chromakey_filter as _build_chromakey_filter_base,
@@ -195,17 +195,3 @@ class ColorGradeEngine:
# ── 便捷函数 ──────────────────────────────────────────────────────────────────
def get_preset_names() -> list[tuple[str, str]]:
"""获取所有预设名称列表.
Returns:
[(preset_key, display_name), ...]
"""
return [(key, PRESET_DISPLAY_NAMES.get(key, key)) for key in PRESET_PARAMS.keys()]
def get_preset_params(preset: str) -> dict[str, float] | None:
"""获取指定预设的参数."""
return PRESET_PARAMS.get(preset)
+3 -5
View File
@@ -22,11 +22,9 @@ from shared.ffmpeg_utils import ( # noqa: F401
# xfade 转场纯逻辑已抽离到 domain 层,这里 re-export 保持向后兼容
from packages.domain.xfade_builder import DEFAULT_TRANSITION_DURATION as _default_transition_duration_base # noqa: F401
from packages.domain.xfade_builder import (
SUPPORTED_TRANSITIONS,
XFADE_TRANSITION_MAP,
XFade_TRANSITION_NAMES,
)
from packages.domain.xfade_builder import SUPPORTED_TRANSITIONS # noqa: F401
from packages.domain.xfade_builder import XFADE_TRANSITION_MAP # noqa: F401
from packages.domain.xfade_builder import XFade_TRANSITION_NAMES # noqa: F401
from packages.domain.xfade_builder import build_xfade_filter_chain as _build_xfade_filter_chain_base
from packages.domain.xfade_builder import chain_filters as _chain_filters_base
from packages.domain.xfade_builder import resolve_xfade_transition as _resolve_xfade_transition_base
@@ -9,16 +9,21 @@ from __future__ import annotations
import logging
# isort: off
from packages.domain.noise_reduction_config import (
NoiseReductionConfig,
NoiseReductionLevel,
NoiseReductionLevel, # noqa: F401
)
from packages.domain.noise_reduction_config import (
apply_noise_reduction_if_needed as _apply_noise_reduction_if_needed_base,
)
from packages.domain.noise_reduction_config import build_afftdn_filter as _build_afftdn_filter_base # noqa: F401 — 向后兼容
from packages.domain.noise_reduction_config import (
build_afftdn_filter as _build_afftdn_filter_base,
) # noqa: F401 — 向后兼容
from packages.domain.noise_reduction_config import build_arnndn_filter as _build_arnndn_filter_base
# isort: on
logger = logging.getLogger(__name__)
+1 -10
View File
@@ -18,22 +18,13 @@ from pathlib import Path
# 向后兼容:POSITION_BOTTOM_CENTER 也从 pip_config 再导出
from packages.domain.pip_config import POSITION_BOTTOM_CENTER # noqa: E402, F401
from packages.domain.pip_config import PiPConfig # noqa: F401
from packages.domain.pip_config import (
ANIMATION_FADE,
ANIMATION_SCALE,
ANIMATION_SLIDE_BOTTOM,
ANIMATION_SLIDE_LEFT,
ANIMATION_SLIDE_RIGHT,
ANIMATION_SLIDE_TOP,
POSITION_BOTTOM_LEFT,
POSITION_BOTTOM_RIGHT,
POSITION_CENTER,
POSITION_CENTER_LEFT,
POSITION_CENTER_RIGHT,
POSITION_TOP_CENTER,
POSITION_TOP_LEFT,
POSITION_TOP_RIGHT,
PiPConfig,
PiPLayerConfig,
)
from packages.domain.pip_config import ( # noqa: F401 — 向后兼容:保留模块级导出
@@ -13,9 +13,6 @@ from pathlib import Path
from typing import Any
from packages.domain.ass_subtitle_builder import (
TITLE_MARGIN_BOTTOM,
TITLE_MARGIN_SIDE,
TITLE_MARGIN_TOP,
build_ass_content,
)
from packages.domain.ass_subtitle_builder import build_ass_style as _build_ass_style_base # noqa: F401 — 向后兼容
@@ -14,16 +14,20 @@ import logging
from pathlib import Path
from typing import Any
# isort: off
from packages.domain.sticker_config import (
POSITION_PRESETS,
STICKER_CATEGORIES,
POSITION_PRESETS, # noqa: F401
STICKER_CATEGORIES, # noqa: F401
ImageStickerConfig,
StickerOverlayResult,
TextStickerConfig,
)
from packages.domain.sticker_config import get_sticker_categories as _get_sticker_categories_base # noqa: F401 向后兼容导出
from packages.domain.sticker_config import (
get_sticker_categories as _get_sticker_categories_base,
) # noqa: F401 向后兼容导出
from packages.domain.sticker_config import parse_stickers_from_config as _parse_stickers_base
from packages.domain.sticker_config import (
# isort: on
resolve_sticker_position,
)
@@ -25,28 +25,22 @@ from __future__ import annotations
import logging
from pathlib import Path
from typing import Any
from video_processing.path_security import PathSecurityError, is_in_allowed_dirs, safe_resolve_path
from packages.domain.subtitle_style import (
ALLOWED_SUBTITLE_EXTENSIONS,
DEFAULT_COLOR,
DEFAULT_FONT,
DEFAULT_FONT_SIZE,
DEFAULT_MAX_CHARS_PER_LINE,
DEFAULT_POSITION,
DEFAULT_STROKE_COLOR,
DEFAULT_STROKE_WIDTH,
POSITION_ALIASES,
POSITION_ALIGNMENT,
SubtitleSegment,
SubtitleStyle,
)
from packages.domain.subtitle_style import escape_ass_text as _escape_ass_text # noqa: F401 向后兼容导出
from packages.domain.subtitle_style import format_ass_time as _format_ass_time
from packages.domain.subtitle_style import hex_to_ass_bgr as _hex_to_ass_bgr
from packages.domain.subtitle_style import hex_to_ass_color as _hex_to_ass_color
from packages.domain.subtitle_style import opacity_to_ass_alpha as _opacity_to_ass_alpha
from packages.domain.subtitle_style import hex_to_ass_bgr as _hex_to_ass_bgr # noqa: F401
from packages.domain.subtitle_style import hex_to_ass_color as _hex_to_ass_color # noqa: F401
from packages.domain.subtitle_style import opacity_to_ass_alpha as _opacity_to_ass_alpha # noqa: F401
from packages.domain.subtitle_style import wrap_text as _wrap_text
logger = logging.getLogger(__name__)
+2 -4
View File
@@ -15,16 +15,14 @@ from __future__ import annotations
import logging
from typing import Any
from packages.domain.trim_config import MIN_TRIM_DURATION # noqa: F401
from packages.domain.trim_config import extract_trim_from_clip_config # noqa: F401
from packages.domain.trim_config import (
MIN_TRIM_DURATION,
TrimConfig,
TrimSegment,
)
from packages.domain.trim_config import build_audio_trim_filter as _build_audio_trim_filter # noqa: F401 — 向后兼容
from packages.domain.trim_config import build_video_trim_filter as _build_video_trim_filter
from packages.domain.trim_config import (
extract_trim_from_clip_config,
)
from packages.domain.trim_config import parse_segments_from_config as _parse_segments_from_config
from packages.domain.trim_config import resolve_segments as _resolve_segments
@@ -53,7 +53,6 @@ from video_processing.tts_engine import TtsEngine
from video_processing.watermark_engine import WatermarkConfig, WatermarkEngine
from packages.domain.render_layer_utils import LAYER_Z_INDEX as _IMPORTED_LAYER_Z_INDEX
from packages.domain.render_layer_utils import can_pass_through as _can_pass_through_pure
from packages.domain.render_layer_utils import clip_adjusted_duration as _clip_adjusted_duration_pure
from packages.domain.render_layer_utils import clip_effective_duration as _clip_effective_duration_pure
from packages.domain.render_layer_utils import clip_playback_speed as _clip_playback_speed_pure
@@ -14,10 +14,9 @@
from __future__ import annotations
import logging
from typing import Any
from packages.domain.watermark_config import WATERMARK_POSITIONS # noqa: F401
from packages.domain.watermark_config import (
WATERMARK_POSITIONS,
WatermarkConfig,
)
from packages.domain.watermark_config import ( # noqa: F401 — 向后兼容
+1 -1
View File
@@ -186,7 +186,7 @@ class PiPConfig:
"""最大 z_index."""
if not self.layers:
return 0
return max(l.z_index for l in self.layers)
return max(layer.z_index for layer in self.layers)
# ── 纯逻辑工具函数 ───────────────────────────────────────────────────────────
+1 -1
View File
@@ -6,7 +6,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any
# ── 常量 ──────────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -11,7 +11,7 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
+1 -1
View File
@@ -10,7 +10,7 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -39,7 +39,7 @@ class TestConstants:
def test_preset_params_complete(self):
assert set(PRESET_PARAMS.keys()) == VALID_PRESETS
for preset, params in PRESET_PARAMS.items():
for _preset, params in PRESET_PARAMS.items():
assert set(params.keys()) == set(ALL_PARAM_KEYS)
def test_default_params_keys(self):
@@ -54,7 +54,7 @@ class TestConstants:
assert min_val <= DEFAULT_PARAMS[key] <= max_val
def test_all_presets_within_ranges(self):
for preset, params in PRESET_PARAMS.items():
for _preset, params in PRESET_PARAMS.items():
for key in ALL_PARAM_KEYS:
min_val, max_val = PARAM_RANGES[key]
assert min_val <= params[key] <= max_val, f"{preset}.{key}={params[key]} out of range"
+1 -1
View File
@@ -208,7 +208,7 @@ class TestPiPConfigFromDict:
}
)
assert cfg.layer_count == 3
assert [l.source for l in cfg.layers] == ["bottom", "mid", "top"]
assert [layer.source for layer in cfg.layers] == ["bottom", "mid", "top"]
def test_invalid_layer_skipped(self):
cfg = PiPConfig.from_dict(
+1 -1
View File
@@ -23,7 +23,7 @@ class TestConstants:
assert len(POSITION_PRESETS) == 9
def test_position_presets_normalized(self):
for name, (x, y) in POSITION_PRESETS.items():
for _name, (x, y) in POSITION_PRESETS.items():
assert 0.0 <= x <= 1.0
assert 0.0 <= y <= 1.0