Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57d45c3219 | |||
| fbf3c5288e | |||
| c8163cbadc |
@@ -12,7 +12,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* 素材相关 API
|
||||
* Phase 1 重构:去掉 project_id,素材直接归属用户
|
||||
*/
|
||||
import apiClient from "./client"
|
||||
import { getOrCreateDefaultProject } from "./projects"
|
||||
|
||||
/** 素材元数据 */
|
||||
export interface AssetMetadata {
|
||||
/** 时长(秒) */
|
||||
duration?: number
|
||||
/** 宽度(像素) */
|
||||
width?: number
|
||||
/** 高度(像素) */
|
||||
height?: number
|
||||
/** 比特率(bps) */
|
||||
bitrate?: number
|
||||
/** 编码格式 */
|
||||
codec?: string
|
||||
/** 帧率 */
|
||||
fps?: number
|
||||
/** 采样率(Hz) */
|
||||
sample_rate?: number
|
||||
/** 声道数 */
|
||||
channels?: number
|
||||
/** 其他扩展字段 */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** 素材分类状态 */
|
||||
export type AssetClassificationStatus = "pending" | "processing" | "completed" | "failed"
|
||||
|
||||
/** 素材条目 */
|
||||
export interface AssetItem {
|
||||
id: string
|
||||
library_id: string
|
||||
name: string
|
||||
storage_key: string
|
||||
mime_type: string
|
||||
metadata: AssetMetadata
|
||||
file_size?: number
|
||||
file_url?: string
|
||||
thumbnail_url?: string
|
||||
/** 时长(秒),视频/音频素材由后端从 metadata 提取到顶层 */
|
||||
duration?: number
|
||||
status?: string
|
||||
classification_status?: AssetClassificationStatus | null
|
||||
quality_score?: number | null
|
||||
tag_ids?: string[]
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
/** 素材库 */
|
||||
export interface AssetLibraryItem {
|
||||
id: string
|
||||
name: string
|
||||
kind: "video" | "voice" | "image"
|
||||
asset_count?: number
|
||||
total_size?: number
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
/** 入库任务 */
|
||||
export interface IngestJob {
|
||||
id: string
|
||||
library_id: string
|
||||
storage_key: string
|
||||
status: "pending" | "processing" | "completed" | "failed"
|
||||
error_message: string
|
||||
result_asset_id: string
|
||||
}
|
||||
|
||||
/** 分类任务 */
|
||||
export interface ClassificationJob {
|
||||
id: string
|
||||
asset_id: string
|
||||
status: "pending" | "processing" | "completed" | "failed"
|
||||
classification: string
|
||||
confidence: number
|
||||
error_message: string
|
||||
}
|
||||
|
||||
/** 素材诊断信息 */
|
||||
export interface AssetDiagnosis {
|
||||
readiness_score: number
|
||||
readiness_label: string
|
||||
total_assets: number
|
||||
ready_assets: number
|
||||
video_assets: number
|
||||
image_assets: number
|
||||
voice_assets: number
|
||||
total_duration_seconds: number
|
||||
estimated_video_count: number
|
||||
used_assets: number
|
||||
unused_assets: number
|
||||
pending_review_assets: number
|
||||
smart_views: Array<{
|
||||
key: string
|
||||
label: string
|
||||
count: number
|
||||
description: string
|
||||
}>
|
||||
gaps: Array<{
|
||||
key: string
|
||||
severity: "critical" | "warning" | "info"
|
||||
message: string
|
||||
recommendation: string
|
||||
}>
|
||||
}
|
||||
|
||||
// ─── 素材诊断 ──────────────────────────────────────────────
|
||||
|
||||
/** 获取素材诊断信息(可选 asset_id 查单素材,否则全局诊断) */
|
||||
export const getAssetDiagnosis = async (assetId?: string): Promise<AssetDiagnosis> => {
|
||||
const params: Record<string, string> = {}
|
||||
if (assetId) params.asset_id = assetId
|
||||
const response = await apiClient.get("/asset-diagnosis", { params })
|
||||
return response.data
|
||||
}
|
||||
|
||||
// ─── 素材库 ────────────────────────────────────────────────
|
||||
|
||||
/** 获取当前用户的所有素材库 */
|
||||
export const getAssetLibraries = async (): Promise<AssetLibraryItem[]> => {
|
||||
const response = await apiClient.get("/asset-libraries")
|
||||
return response.data.items || []
|
||||
}
|
||||
|
||||
/** 创建素材库(自动获取或创建默认项目以提供 project_id) */
|
||||
export const createAssetLibrary = async (data: {
|
||||
name: string
|
||||
kind: "video" | "voice" | "image"
|
||||
}): Promise<AssetLibraryItem> => {
|
||||
// 后端要求 project_id,前端自动管理默认项目
|
||||
const project = await getOrCreateDefaultProject()
|
||||
const response = await apiClient.post("/asset-libraries", {
|
||||
project_id: project.id,
|
||||
...data,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 确保项目下指定 kind 的默认素材库存在(不存在则自动创建) */
|
||||
export const ensureDefaultLibrary = async (data: {
|
||||
project_id: string
|
||||
kind: "video" | "voice" | "image"
|
||||
}): Promise<AssetLibraryItem> => {
|
||||
const response = await apiClient.post("/asset-libraries/ensure-default", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 删除素材库 */
|
||||
export const deleteAssetLibrary = async (libraryId: string): Promise<void> => {
|
||||
await apiClient.delete(`/asset-libraries/${libraryId}`)
|
||||
}
|
||||
|
||||
// ─── 素材 ──────────────────────────────────────────────────
|
||||
|
||||
/** 获取素材库下的所有素材 */
|
||||
export const getAssets = async (
|
||||
libraryId: string,
|
||||
options?: { status?: string; page?: number; page_size?: number },
|
||||
): Promise<{ items: AssetItem[]; total: number }> => {
|
||||
const params: Record<string, string | number> = { library_id: libraryId }
|
||||
// 默认拉取所有非删除状态的素材(ready/ingesting/processing/uploading/error/failed)
|
||||
// 让用户能看到"处理中"的素材,不会以为上传失败了
|
||||
if (options?.status) {
|
||||
params.status = options.status
|
||||
}
|
||||
if (options?.page) params.page = options.page
|
||||
if (options?.page_size) params.page_size = options.page_size
|
||||
const response = await apiClient.get("/assets", { params })
|
||||
const data = response.data || {}
|
||||
const items: AssetItem[] = data.items || []
|
||||
const total: number = typeof data.total === "number" ? data.total : items.length
|
||||
return { items, total }
|
||||
}
|
||||
|
||||
/** 按类型获取素材(如 voice/video/image),支持可选筛选 */
|
||||
export const getAssetsByKind = async (
|
||||
kind: string,
|
||||
filters?: {
|
||||
keyword?: string
|
||||
gender?: string
|
||||
style?: string
|
||||
tag_ids?: string[]
|
||||
limit?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
},
|
||||
): Promise<AssetItem[]> => {
|
||||
const params: Record<string, string | number> = { kind }
|
||||
if (filters?.keyword) params.keyword = filters.keyword
|
||||
if (filters?.gender) params.gender = filters.gender
|
||||
if (filters?.style) params.style = filters.style
|
||||
if (filters?.tag_ids?.length) params.tag_ids = filters.tag_ids.join(",")
|
||||
if (filters?.limit) params.limit = filters.limit
|
||||
if (filters?.page) params.page = filters.page
|
||||
if (filters?.page_size) params.page_size = filters.page_size
|
||||
const response = await apiClient.get("/assets", { params })
|
||||
return response.data.items || []
|
||||
}
|
||||
|
||||
/** 创建素材(上传文件后调用,附带 metadata) */
|
||||
export const createAsset = async (data: {
|
||||
library_id: string
|
||||
name: string
|
||||
storage_key: string
|
||||
mime_type: string
|
||||
metadata?: AssetMetadata
|
||||
}): Promise<AssetItem> => {
|
||||
const response = await apiClient.post("/assets", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 更新素材(名称、metadata 等) */
|
||||
export const updateAsset = async (
|
||||
assetId: string,
|
||||
data: { name?: string; metadata?: AssetMetadata },
|
||||
): Promise<AssetItem> => {
|
||||
const response = await apiClient.put(`/assets/${assetId}`, data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 更新素材审核状态 */
|
||||
export const updateAssetReviewStatus = async (
|
||||
assetId: string,
|
||||
reviewStatus: "pending_review" | "approved" | "rejected",
|
||||
): Promise<AssetItem> => {
|
||||
const response = await apiClient.patch(`/assets/${assetId}/review`, {
|
||||
review_status: reviewStatus,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 删除素材 */
|
||||
export const deleteAsset = async (assetId: string): Promise<void> => {
|
||||
await apiClient.delete(`/assets/${assetId}`)
|
||||
}
|
||||
|
||||
// ─── 上传 ──────────────────────────────────────────────────
|
||||
|
||||
/** 表单上传素材(小文件) */
|
||||
export const uploadAsset = async (
|
||||
formData: FormData,
|
||||
): Promise<{ storage_key: string; ingest_job_id: string; url: string }> => {
|
||||
const response = await apiClient.post("/upload", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
timeout: 30 * 60 * 1000,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 预签名直传准备 */
|
||||
export const prepareDirectUpload = async (data: {
|
||||
project_id: string
|
||||
library_id: string
|
||||
filename: string
|
||||
content_type: string
|
||||
file_size: number
|
||||
}): Promise<{
|
||||
upload_url: string
|
||||
method: string
|
||||
storage_key: string
|
||||
expires_at: string
|
||||
fields: Record<string, string>
|
||||
max_size_bytes: number
|
||||
}> => {
|
||||
const response = await apiClient.post("/upload/direct/prepare", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 直传完成确认 */
|
||||
export const completeDirectUpload = async (data: {
|
||||
project_id: string
|
||||
library_id: string
|
||||
storage_key: string
|
||||
}): Promise<{ storage_key: string; ingest_job_id: string }> => {
|
||||
const response = await apiClient.post("/upload/direct/complete", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 直传上传(大文件推荐),支持可选进度回调 */
|
||||
export const uploadAssetDirect = async (data: {
|
||||
file: File
|
||||
library_id: string
|
||||
onProgress?: (percent: number) => void
|
||||
}): Promise<{ storage_key: string; ingest_job_id: string }> => {
|
||||
// 后端要求 project_id,前端自动获取默认项目
|
||||
const project = await getOrCreateDefaultProject()
|
||||
|
||||
const prepared = await prepareDirectUpload({
|
||||
project_id: project.id,
|
||||
library_id: data.library_id,
|
||||
filename: data.file.name,
|
||||
content_type: data.file.type || "application/octet-stream",
|
||||
file_size: data.file.size,
|
||||
})
|
||||
|
||||
const directForm = new FormData()
|
||||
Object.entries(prepared.fields).forEach(([key, value]) => directForm.append(key, value))
|
||||
directForm.append("file", data.file)
|
||||
|
||||
// 使用 XMLHttpRequest 以获取上传进度 + 超时控制 + 详细错误诊断
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open(prepared.method, prepared.upload_url)
|
||||
|
||||
// 超时 10 分钟
|
||||
xhr.timeout = 10 * 60 * 1000
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable && data.onProgress) {
|
||||
data.onProgress(Math.round((e.loaded / e.total) * 100))
|
||||
}
|
||||
}
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve()
|
||||
} else {
|
||||
// 解析 OSS 返回的 XML 错误信息
|
||||
let ossError = ""
|
||||
try {
|
||||
const codeMatch = xhr.responseText.match(/<Code>([^<]+)<\/Code>/)
|
||||
const msgMatch = xhr.responseText.match(/<Message>([^<]+)<\/Message>/)
|
||||
if (codeMatch || msgMatch) {
|
||||
ossError = ` [OSS: ${codeMatch?.[1] || "unknown"} - ${msgMatch?.[1] || "unknown"}]`
|
||||
}
|
||||
} catch {
|
||||
// 无法解析响应体
|
||||
}
|
||||
const detail = `OSS 直传失败: HTTP ${xhr.status} ${xhr.statusText}${ossError}`
|
||||
console.error("[OSS Upload] 直传失败:", {
|
||||
url: prepared.upload_url,
|
||||
storage_key: prepared.storage_key,
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
})
|
||||
reject(new Error(detail))
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
console.error("[OSS Upload] 网络错误:", {
|
||||
url: prepared.upload_url,
|
||||
storage_key: prepared.storage_key,
|
||||
})
|
||||
reject(new Error("OSS 上传网络错误,请检查网络连接"))
|
||||
}
|
||||
xhr.ontimeout = () => {
|
||||
console.error("[OSS Upload] 上传超时:", {
|
||||
url: prepared.upload_url,
|
||||
storage_key: prepared.storage_key,
|
||||
})
|
||||
reject(new Error("OSS 上传超时(10分钟),请检查网络或尝试更小的文件"))
|
||||
}
|
||||
xhr.send(directForm)
|
||||
})
|
||||
|
||||
return completeDirectUpload({
|
||||
project_id: project.id,
|
||||
library_id: data.library_id,
|
||||
storage_key: prepared.storage_key,
|
||||
})
|
||||
}
|
||||
|
||||
// ─── 入库 / 分类任务 ───────────────────────────────────────
|
||||
|
||||
/** 查询入库任务状态 */
|
||||
export const getIngestJob = async (jobId: string): Promise<IngestJob> => {
|
||||
const response = await apiClient.get(`/ingest-jobs/${jobId}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 提交素材分类任务 */
|
||||
export const submitClassificationJob = async (data: {
|
||||
asset_id: string
|
||||
}): Promise<ClassificationJob> => {
|
||||
const response = await apiClient.post("/classification-jobs", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 查询分类任务状态 */
|
||||
export const getClassificationJob = async (jobId: string): Promise<ClassificationJob> => {
|
||||
const response = await apiClient.get(`/classification-jobs/${jobId}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
// ─── 批量操作 ───────────────────────────────────────────────
|
||||
|
||||
/** 批量操作结果 */
|
||||
export interface BatchOperationResult {
|
||||
succeeded: string[]
|
||||
failed: string[]
|
||||
total: number
|
||||
success_count: number
|
||||
failure_count: number
|
||||
}
|
||||
|
||||
/** 统一批量操作结果归一化,防御后端字段缺失或格式不一致 */
|
||||
const normalizeBatchResult = (raw: Record<string, unknown>): BatchOperationResult => {
|
||||
const succeeded = Array.isArray(raw.succeeded) ? (raw.succeeded as string[]) : []
|
||||
const failed = Array.isArray(raw.failed) ? (raw.failed as string[]) : []
|
||||
const success_count = typeof raw.success_count === "number" ? raw.success_count : succeeded.length
|
||||
const failure_count = typeof raw.failure_count === "number" ? raw.failure_count : failed.length
|
||||
const total = typeof raw.total === "number" ? raw.total : success_count + failure_count
|
||||
return { succeeded, failed, total, success_count, failure_count }
|
||||
}
|
||||
|
||||
/** 批量删除素材 */
|
||||
export const batchDeleteAssets = async (assetIds: string[]): Promise<BatchOperationResult> => {
|
||||
const response = await apiClient.post("/assets/batch-delete", {
|
||||
asset_ids: assetIds,
|
||||
})
|
||||
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 批量打标签 */
|
||||
export const batchTagAssets = async (data: {
|
||||
asset_ids: string[]
|
||||
tags: string[]
|
||||
mode: "add" | "replace"
|
||||
}): Promise<BatchOperationResult> => {
|
||||
const response = await apiClient.post("/assets/batch-tag", data)
|
||||
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 批量改分类 */
|
||||
export const batchClassifyAssets = async (data: {
|
||||
asset_ids: string[]
|
||||
category: string
|
||||
}): Promise<BatchOperationResult> => {
|
||||
const response = await apiClient.post("/assets/batch-classify", data)
|
||||
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 批量智能标记 */
|
||||
export const batchMarkAssets = async (data: {
|
||||
asset_ids: string[]
|
||||
smart_view: "recommended" | "caution" | "high_risk"
|
||||
}): Promise<BatchOperationResult> => {
|
||||
const response = await apiClient.post("/assets/batch-mark", data)
|
||||
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* 素材 CRUD API
|
||||
*/
|
||||
import apiClient from "../client"
|
||||
import type { AssetItem, AssetMetadata } from "./types"
|
||||
|
||||
/** 获取素材库下的所有素材 */
|
||||
export const getAssets = async (
|
||||
libraryId: string,
|
||||
options?: {
|
||||
status?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
},
|
||||
): Promise<{ items: AssetItem[]; total: number }> => {
|
||||
const params: Record<string, string | number> = { library_id: libraryId }
|
||||
if (options?.status) params.status = options.status
|
||||
if (options?.page) params.page = options.page
|
||||
if (options?.page_size) params.page_size = options.page_size
|
||||
const response = await apiClient.get("/assets", { params })
|
||||
const data = response.data || {}
|
||||
const items: AssetItem[] = data.items || []
|
||||
const total: number = typeof data.total === "number" ? data.total : items.length
|
||||
return { items, total }
|
||||
}
|
||||
|
||||
/** 按类型获取素材(如 voice/video/image),支持可选筛选 */
|
||||
export const getAssetsByKind = async (
|
||||
kind: string,
|
||||
filters?: {
|
||||
keyword?: string
|
||||
gender?: string
|
||||
style?: string
|
||||
tag_ids?: string[]
|
||||
limit?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
},
|
||||
): Promise<AssetItem[]> => {
|
||||
const params: Record<string, string | number> = { kind }
|
||||
if (filters?.keyword) params.keyword = filters.keyword
|
||||
if (filters?.gender) params.gender = filters.gender
|
||||
if (filters?.style) params.style = filters.style
|
||||
if (filters?.tag_ids?.length) params.tag_ids = filters.tag_ids.join(",")
|
||||
if (filters?.limit) params.limit = filters.limit
|
||||
if (filters?.page) params.page = filters.page
|
||||
if (filters?.page_size) params.page_size = filters.page_size
|
||||
const response = await apiClient.get("/assets", { params })
|
||||
return response.data.items || []
|
||||
}
|
||||
|
||||
/** 创建素材(上传文件后调用,附带 metadata) */
|
||||
export const createAsset = async (data: {
|
||||
library_id: string
|
||||
name: string
|
||||
storage_key: string
|
||||
mime_type: string
|
||||
metadata?: AssetMetadata
|
||||
}): Promise<AssetItem> => {
|
||||
const response = await apiClient.post("/assets", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 更新素材(名称、metadata 等) */
|
||||
export const updateAsset = async (
|
||||
assetId: string,
|
||||
data: { name?: string; metadata?: AssetMetadata },
|
||||
): Promise<AssetItem> => {
|
||||
const response = await apiClient.put(`/assets/${assetId}`, data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 更新素材审核状态 */
|
||||
export const updateAssetReviewStatus = async (
|
||||
assetId: string,
|
||||
reviewStatus: "pending_review" | "approved" | "rejected",
|
||||
): Promise<AssetItem> => {
|
||||
const response = await apiClient.patch(`/assets/${assetId}/review`, {
|
||||
review_status: reviewStatus,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 删除素材 */
|
||||
export const deleteAsset = async (assetId: string): Promise<void> => {
|
||||
await apiClient.delete(`/assets/${assetId}`)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* 素材批量操作 API
|
||||
*/
|
||||
import apiClient from "../client"
|
||||
import type { BatchOperationResult } from "./types"
|
||||
|
||||
/** 统一批量操作结果归一化,防御后端字段缺失或格式不一致 */
|
||||
export const normalizeBatchResult = (raw: Record<string, unknown>): BatchOperationResult => {
|
||||
const succeeded = Array.isArray(raw.succeeded) ? (raw.succeeded as string[]) : []
|
||||
const failed = Array.isArray(raw.failed) ? (raw.failed as string[]) : []
|
||||
const success_count = typeof raw.success_count === "number" ? raw.success_count : succeeded.length
|
||||
const failure_count = typeof raw.failure_count === "number" ? raw.failure_count : failed.length
|
||||
const total = typeof raw.total === "number" ? raw.total : success_count + failure_count
|
||||
return { succeeded, failed, total, success_count, failure_count }
|
||||
}
|
||||
|
||||
/** 批量删除素材 */
|
||||
export const batchDeleteAssets = async (assetIds: string[]): Promise<BatchOperationResult> => {
|
||||
const response = await apiClient.post("/assets/batch-delete", {
|
||||
asset_ids: assetIds,
|
||||
})
|
||||
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 批量打标签 */
|
||||
export const batchTagAssets = async (data: {
|
||||
asset_ids: string[]
|
||||
tags: string[]
|
||||
mode: "add" | "replace"
|
||||
}): Promise<BatchOperationResult> => {
|
||||
const response = await apiClient.post("/assets/batch-tag", data)
|
||||
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 批量改分类 */
|
||||
export const batchClassifyAssets = async (data: {
|
||||
asset_ids: string[]
|
||||
category: string
|
||||
}): Promise<BatchOperationResult> => {
|
||||
const response = await apiClient.post("/assets/batch-classify", data)
|
||||
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
|
||||
}
|
||||
|
||||
/** 批量智能标记 */
|
||||
export const batchMarkAssets = async (data: {
|
||||
asset_ids: string[]
|
||||
smart_view: "recommended" | "caution" | "high_risk"
|
||||
}): Promise<BatchOperationResult> => {
|
||||
const response = await apiClient.post("/assets/batch-mark", data)
|
||||
return normalizeBatchResult((response.data || {}) as Record<string, unknown>)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* 素材诊断 API
|
||||
*/
|
||||
import apiClient from "../client"
|
||||
import type { AssetDiagnosis } from "./types"
|
||||
|
||||
/** 获取素材诊断信息(可选 asset_id 查单素材,否则全局诊断) */
|
||||
export const getAssetDiagnosis = async (assetId?: string): Promise<AssetDiagnosis> => {
|
||||
const params: Record<string, string> = {}
|
||||
if (assetId) params.asset_id = assetId
|
||||
const response = await apiClient.get("/asset-diagnosis", { params })
|
||||
return response.data
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* 素材相关 API — 按模块拆分后的统一入口
|
||||
* 保持与原 assets.ts 相同的导出结构,向后兼容
|
||||
*/
|
||||
|
||||
// 类型
|
||||
export type {
|
||||
AssetMetadata,
|
||||
AssetClassificationStatus,
|
||||
AssetItem,
|
||||
AssetLibraryItem,
|
||||
IngestJob,
|
||||
ClassificationJob,
|
||||
AssetDiagnosis,
|
||||
BatchOperationResult,
|
||||
UploadResult,
|
||||
DirectUploadPrepareResult,
|
||||
DirectUploadCompleteResult,
|
||||
} from "./types"
|
||||
|
||||
// 素材诊断
|
||||
export { getAssetDiagnosis } from "./diagnosis"
|
||||
|
||||
// 素材库
|
||||
export {
|
||||
getAssetLibraries,
|
||||
createAssetLibrary,
|
||||
ensureDefaultLibrary,
|
||||
deleteAssetLibrary,
|
||||
} from "./libraries"
|
||||
|
||||
// 素材 CRUD
|
||||
export {
|
||||
getAssets,
|
||||
getAssetsByKind,
|
||||
createAsset,
|
||||
updateAsset,
|
||||
updateAssetReviewStatus,
|
||||
deleteAsset,
|
||||
} from "./assets"
|
||||
|
||||
// 上传
|
||||
export { uploadAsset, prepareDirectUpload, completeDirectUpload, uploadAssetDirect } from "./upload"
|
||||
|
||||
// 任务
|
||||
export { getIngestJob, submitClassificationJob, getClassificationJob } from "./jobs"
|
||||
|
||||
// 批量操作
|
||||
export {
|
||||
normalizeBatchResult,
|
||||
batchDeleteAssets,
|
||||
batchTagAssets,
|
||||
batchClassifyAssets,
|
||||
batchMarkAssets,
|
||||
} from "./batch"
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* 入库任务 & 分类任务 API
|
||||
*/
|
||||
import apiClient from "../client"
|
||||
import type { IngestJob, ClassificationJob } from "./types"
|
||||
|
||||
/** 查询入库任务状态 */
|
||||
export const getIngestJob = async (jobId: string): Promise<IngestJob> => {
|
||||
const response = await apiClient.get(`/ingest-jobs/${jobId}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 提交素材分类任务 */
|
||||
export const submitClassificationJob = async (data: {
|
||||
asset_id: string
|
||||
}): Promise<ClassificationJob> => {
|
||||
const response = await apiClient.post("/classification-jobs", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 查询分类任务状态 */
|
||||
export const getClassificationJob = async (jobId: string): Promise<ClassificationJob> => {
|
||||
const response = await apiClient.get(`/classification-jobs/${jobId}`)
|
||||
return response.data
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* 素材库 API
|
||||
*/
|
||||
import apiClient from "../client"
|
||||
import { getOrCreateDefaultProject } from "../projects"
|
||||
import type { AssetLibraryItem } from "./types"
|
||||
|
||||
/** 获取当前用户的所有素材库 */
|
||||
export const getAssetLibraries = async (): Promise<AssetLibraryItem[]> => {
|
||||
const response = await apiClient.get("/asset-libraries")
|
||||
return response.data.items || []
|
||||
}
|
||||
|
||||
/** 创建素材库(自动获取或创建默认项目以提供 project_id) */
|
||||
export const createAssetLibrary = async (data: {
|
||||
name: string
|
||||
kind: "video" | "voice" | "image"
|
||||
}): Promise<AssetLibraryItem> => {
|
||||
const project = await getOrCreateDefaultProject()
|
||||
const response = await apiClient.post("/asset-libraries", {
|
||||
project_id: project.id,
|
||||
...data,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 确保项目下指定 kind 的默认素材库存在 */
|
||||
export const ensureDefaultLibrary = async (data: {
|
||||
project_id: string
|
||||
kind: "video" | "voice" | "image"
|
||||
}): Promise<AssetLibraryItem> => {
|
||||
const response = await apiClient.post("/asset-libraries/ensure-default", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 删除素材库 */
|
||||
export const deleteAssetLibrary = async (libraryId: string): Promise<void> => {
|
||||
await apiClient.delete(`/asset-libraries/${libraryId}`)
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* 素材相关类型定义
|
||||
*/
|
||||
|
||||
/** 素材元数据 */
|
||||
export interface AssetMetadata {
|
||||
/** 时长(秒) */
|
||||
duration?: number
|
||||
/** 宽度(像素) */
|
||||
width?: number
|
||||
/** 高度(像素) */
|
||||
height?: number
|
||||
/** 比特率(bps) */
|
||||
bitrate?: number
|
||||
/** 编码格式 */
|
||||
codec?: string
|
||||
/** 帧率 */
|
||||
fps?: number
|
||||
/** 采样率(Hz) */
|
||||
sample_rate?: number
|
||||
/** 声道数 */
|
||||
channels?: number
|
||||
/** 其他扩展字段 */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** 素材分类状态 */
|
||||
export type AssetClassificationStatus = "pending" | "processing" | "completed" | "failed"
|
||||
|
||||
/** 素材条目 */
|
||||
export interface AssetItem {
|
||||
id: string
|
||||
library_id: string
|
||||
name: string
|
||||
storage_key: string
|
||||
mime_type: string
|
||||
metadata: AssetMetadata
|
||||
file_size?: number
|
||||
file_url?: string
|
||||
thumbnail_url?: string
|
||||
/** 时长(秒),视频/音频素材由后端从 metadata 提取到顶层 */
|
||||
duration?: number
|
||||
status?: string
|
||||
classification_status?: AssetClassificationStatus | null
|
||||
quality_score?: number | null
|
||||
tag_ids?: string[]
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
/** 素材库 */
|
||||
export interface AssetLibraryItem {
|
||||
id: string
|
||||
name: string
|
||||
kind: "video" | "voice" | "image"
|
||||
asset_count?: number
|
||||
total_size?: number
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
/** 入库任务 */
|
||||
export interface IngestJob {
|
||||
id: string
|
||||
library_id: string
|
||||
storage_key: string
|
||||
status: "pending" | "processing" | "completed" | "failed"
|
||||
error_message: string
|
||||
result_asset_id: string
|
||||
}
|
||||
|
||||
/** 分类任务 */
|
||||
export interface ClassificationJob {
|
||||
id: string
|
||||
asset_id: string
|
||||
status: "pending" | "processing" | "completed" | "failed"
|
||||
classification: string
|
||||
confidence: number
|
||||
error_message: string
|
||||
}
|
||||
|
||||
/** 素材诊断信息 */
|
||||
export interface AssetDiagnosis {
|
||||
readiness_score: number
|
||||
readiness_label: string
|
||||
total_assets: number
|
||||
ready_assets: number
|
||||
video_assets: number
|
||||
image_assets: number
|
||||
voice_assets: number
|
||||
total_duration_seconds: number
|
||||
estimated_video_count: number
|
||||
used_assets: number
|
||||
unused_assets: number
|
||||
pending_review_assets: number
|
||||
smart_views: Array<{
|
||||
key: string
|
||||
label: string
|
||||
count: number
|
||||
description: string
|
||||
}>
|
||||
gaps: Array<{
|
||||
key: string
|
||||
severity: "critical" | "warning" | "info"
|
||||
message: string
|
||||
recommendation: string
|
||||
}>
|
||||
}
|
||||
|
||||
/** 批量操作结果 */
|
||||
export interface BatchOperationResult {
|
||||
succeeded: string[]
|
||||
failed: string[]
|
||||
total: number
|
||||
success_count: number
|
||||
failure_count: number
|
||||
}
|
||||
|
||||
/** 上传返回 */
|
||||
export interface UploadResult {
|
||||
storage_key: string
|
||||
ingest_job_id: string
|
||||
url: string
|
||||
}
|
||||
|
||||
/** 预签名直传准备返回 */
|
||||
export interface DirectUploadPrepareResult {
|
||||
upload_url: string
|
||||
method: string
|
||||
storage_key: string
|
||||
expires_at: string
|
||||
fields: Record<string, string>
|
||||
max_size_bytes: number
|
||||
}
|
||||
|
||||
/** 直传完成确认返回 */
|
||||
export interface DirectUploadCompleteResult {
|
||||
storage_key: string
|
||||
ingest_job_id: string
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* 上传相关 API(表单上传 + OSS 直传)
|
||||
*/
|
||||
import apiClient from "../client"
|
||||
import { getOrCreateDefaultProject } from "../projects"
|
||||
import type { UploadResult, DirectUploadPrepareResult, DirectUploadCompleteResult } from "./types"
|
||||
|
||||
/** 表单上传素材(小文件) */
|
||||
export const uploadAsset = async (formData: FormData): Promise<UploadResult> => {
|
||||
const response = await apiClient.post("/upload", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
timeout: 30 * 60 * 1000,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 预签名直传准备 */
|
||||
export const prepareDirectUpload = async (data: {
|
||||
project_id: string
|
||||
library_id: string
|
||||
filename: string
|
||||
content_type: string
|
||||
file_size: number
|
||||
}): Promise<DirectUploadPrepareResult> => {
|
||||
const response = await apiClient.post("/upload/direct/prepare", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 直传完成确认 */
|
||||
export const completeDirectUpload = async (data: {
|
||||
project_id: string
|
||||
library_id: string
|
||||
storage_key: string
|
||||
}): Promise<DirectUploadCompleteResult> => {
|
||||
const response = await apiClient.post("/upload/direct/complete", data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** 直传上传(大文件推荐),支持可选进度回调 */
|
||||
export const uploadAssetDirect = async (data: {
|
||||
file: File
|
||||
library_id: string
|
||||
onProgress?: (percent: number) => void
|
||||
}): Promise<DirectUploadCompleteResult> => {
|
||||
const project = await getOrCreateDefaultProject()
|
||||
|
||||
const prepared = await prepareDirectUpload({
|
||||
project_id: project.id,
|
||||
library_id: data.library_id,
|
||||
filename: data.file.name,
|
||||
content_type: data.file.type || "application/octet-stream",
|
||||
file_size: data.file.size,
|
||||
})
|
||||
|
||||
const directForm = new FormData()
|
||||
Object.entries(prepared.fields).forEach(([key, value]) => directForm.append(key, value))
|
||||
directForm.append("file", data.file)
|
||||
|
||||
// 使用 XMLHttpRequest 以获取上传进度 + 超时控制 + 详细错误诊断
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open(prepared.method, prepared.upload_url)
|
||||
|
||||
// 超时 10 分钟
|
||||
xhr.timeout = 10 * 60 * 1000
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable && data.onProgress) {
|
||||
data.onProgress(Math.round((e.loaded / e.total) * 100))
|
||||
}
|
||||
}
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve()
|
||||
} else {
|
||||
// 解析 OSS 返回的 XML 错误信息
|
||||
let ossError = ""
|
||||
try {
|
||||
const codeMatch = xhr.responseText.match(/<Code>([^<]+)<\/Code>/)
|
||||
const msgMatch = xhr.responseText.match(/<Message>([^<]+)<\/Message>/)
|
||||
if (codeMatch || msgMatch) {
|
||||
ossError = ` [OSS: ${codeMatch?.[1] || "unknown"} - ${msgMatch?.[1] || "unknown"}]`
|
||||
}
|
||||
} catch {
|
||||
// 无法解析响应体
|
||||
}
|
||||
const detail = `OSS 直传失败: HTTP ${xhr.status} ${xhr.statusText}${ossError}`
|
||||
console.error("[OSS Upload] 直传失败:", {
|
||||
url: prepared.upload_url,
|
||||
storage_key: prepared.storage_key,
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
})
|
||||
reject(new Error(detail))
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
console.error("[OSS Upload] 网络错误:", {
|
||||
url: prepared.upload_url,
|
||||
storage_key: prepared.storage_key,
|
||||
})
|
||||
reject(new Error("OSS 上传网络错误,请检查网络连接"))
|
||||
}
|
||||
xhr.ontimeout = () => {
|
||||
console.error("[OSS Upload] 上传超时:", {
|
||||
url: prepared.upload_url,
|
||||
storage_key: prepared.storage_key,
|
||||
})
|
||||
reject(new Error("OSS 上传超时(10分钟),请检查网络或尝试更小的文件"))
|
||||
}
|
||||
xhr.send(directForm)
|
||||
})
|
||||
|
||||
return completeDirectUpload({
|
||||
project_id: project.id,
|
||||
library_id: data.library_id,
|
||||
storage_key: prepared.storage_key,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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 — 向后兼容
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
# ── 纯逻辑工具函数 ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
# ── 常量 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user