feat(phase7): wire asset upload and ingest flow
This commit is contained in:
@@ -9,6 +9,26 @@ from packages.application import SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/{job_id}", response_model=IngestJobResponse)
|
||||
def get_ingest_job(
|
||||
job_id: str,
|
||||
ingest_job_repository: SQLAlchemyIngestJobRepository = Depends(get_ingest_job_repository),
|
||||
) -> IngestJobResponse:
|
||||
job = ingest_job_repository.get(job_id)
|
||||
if job is None:
|
||||
raise ValueError(f"IngestJob {job_id} not found")
|
||||
return IngestJobResponse(
|
||||
id=job.id,
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
library_id=job.library_id,
|
||||
storage_key=job.storage_key,
|
||||
status=job.status.value,
|
||||
error_message=job.error_message,
|
||||
result_asset_id=job.result_asset_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=IngestJobResponse)
|
||||
def submit_ingest_job(
|
||||
request: SubmitIngestJobRequest,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, Form
|
||||
from fastapi import APIRouter, Depends, File, Form, UploadFile
|
||||
from uuid import uuid4
|
||||
|
||||
from app.core.celery_app import celery_app
|
||||
from app.dependencies import get_ingest_job_repository
|
||||
from app.core.storage import MinIOService, get_minio_service
|
||||
from app.dependencies import get_ingest_job_repository
|
||||
from app.schemas.upload import UploadAssetResponse
|
||||
from packages.adapters.sqlalchemy_impl import SQLAlchemyIngestJobRepository
|
||||
from packages.application import SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
@@ -16,64 +16,20 @@ async def upload_asset(
|
||||
file: UploadFile = File(..., description="要上传的文件(视频、音频、图片等)"),
|
||||
workspace_id: str = Form(..., description="工作空间 ID"),
|
||||
project_id: str = Form(..., description="项目 ID"),
|
||||
library_id: str = Form(..., description="资产库 ID"),
|
||||
library_id: str = Form(..., description="素材库 ID"),
|
||||
ingest_job_repository: SQLAlchemyIngestJobRepository = Depends(get_ingest_job_repository),
|
||||
storage_service: MinIOService = Depends(get_minio_service),
|
||||
) -> UploadAssetResponse:
|
||||
"""
|
||||
上传素材文件并触发导入流水线。
|
||||
|
||||
## 功能说明
|
||||
|
||||
1. **接收文件**:支持 multipart/form-data 上传
|
||||
2. **存储到 MinIO**:自动存储到对象存储
|
||||
3. **生成存储键**:格式为 `uploads/{id}/{filename}`
|
||||
4. **提交导入任务**:创建 IngestJob 记录
|
||||
5. **异步处理**:通过 Celery 队列处理
|
||||
|
||||
## 支持的文件类型
|
||||
|
||||
- **视频**:MP4, MOV, AVI, MKV 等
|
||||
- **音频**:MP3, WAV, AAC 等
|
||||
- **图片**:JPG, PNG, GIF, WebP 等
|
||||
|
||||
## 请求示例
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/v1/upload" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "file=@/path/to/video.mp4" \
|
||||
-F "workspace_id=ws_123" \
|
||||
-F "project_id=proj_456" \
|
||||
-F "library_id=lib_789"
|
||||
```
|
||||
|
||||
## 响应说明
|
||||
|
||||
- `storage_key`: 文件在 MinIO 中的存储路径
|
||||
- `ingest_job_id`: 导入任务 ID,用于追踪处理状态
|
||||
- `url`: 文件的公开访问 URL
|
||||
|
||||
## 后续流程
|
||||
|
||||
上传成功后,系统会:
|
||||
1. 自动提取文件元数据(时长、分辨率等)
|
||||
2. 生成缩略图
|
||||
3. 进行场景分割(视频)
|
||||
4. 创建 Asset 记录
|
||||
"""
|
||||
# Generate storage key
|
||||
"""上传素材文件并触发导入流水线。"""
|
||||
file_id = uuid4().hex[:8]
|
||||
storage_key = f"uploads/{file_id}/{file.filename}"
|
||||
|
||||
# Upload file to MinIO
|
||||
file_url = storage_service.upload_file(
|
||||
file.file,
|
||||
storage_key,
|
||||
content_type=file.content_type or "application/octet-stream",
|
||||
)
|
||||
|
||||
# Submit ingest job
|
||||
use_case = SubmitIngestJobUseCase(ingest_job_repository)
|
||||
job = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 素材相关 API
|
||||
*/
|
||||
import apiClient from './client';
|
||||
|
||||
export interface AssetItem {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
project_id: string;
|
||||
library_id: string;
|
||||
name: string;
|
||||
storage_key: string;
|
||||
mime_type: string;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AssetLibraryItem {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
project_id: string;
|
||||
name: string;
|
||||
kind: 'video' | 'voice';
|
||||
}
|
||||
|
||||
export interface IngestJob {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
project_id: string;
|
||||
library_id: string;
|
||||
storage_key: string;
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
error_message: string;
|
||||
result_asset_id: string;
|
||||
}
|
||||
|
||||
export const getAssetLibraries = async (projectId: string): Promise<AssetLibraryItem[]> => {
|
||||
const response = await apiClient.get('/asset-libraries', {
|
||||
params: { project_id: projectId },
|
||||
});
|
||||
return response.data.items;
|
||||
};
|
||||
|
||||
export const createAssetLibrary = async (data: {
|
||||
workspace_id: string;
|
||||
project_id: string;
|
||||
name: string;
|
||||
kind: 'video' | 'voice';
|
||||
}): Promise<AssetLibraryItem> => {
|
||||
const response = await apiClient.post('/asset-libraries', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAssets = async (libraryId: string): Promise<AssetItem[]> => {
|
||||
const response = await apiClient.get('/assets', {
|
||||
params: { library_id: libraryId },
|
||||
});
|
||||
return response.data.items;
|
||||
};
|
||||
|
||||
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' },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getIngestJob = async (jobId: string): Promise<IngestJob> => {
|
||||
const response = await apiClient.get(`/ingest-jobs/${jobId}`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
import apiClient from './client';
|
||||
|
||||
// 类型定义
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -32,19 +31,16 @@ export interface InviteMemberRequest {
|
||||
role: 'admin' | 'member' | 'viewer';
|
||||
}
|
||||
|
||||
// 获取工作空间列表
|
||||
export const getWorkspaces = async (): Promise<Workspace[]> => {
|
||||
const response = await apiClient.get('/workspaces');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 获取工作空间详情
|
||||
export const getWorkspace = async (id: string): Promise<Workspace> => {
|
||||
const response = await apiClient.get(`/workspaces/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 创建工作空间
|
||||
export const createWorkspace = async (
|
||||
data: CreateWorkspaceRequest
|
||||
): Promise<Workspace> => {
|
||||
@@ -52,13 +48,11 @@ export const createWorkspace = async (
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 获取成员列表
|
||||
export const getMembers = async (workspaceId: string): Promise<WorkspaceMember[]> => {
|
||||
const response = await apiClient.get(`/workspaces/${workspaceId}/members`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 邀请成员
|
||||
export const inviteMember = async (
|
||||
workspaceId: string,
|
||||
data: InviteMemberRequest
|
||||
@@ -67,7 +61,6 @@ export const inviteMember = async (
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 移除成员
|
||||
export const removeMember = async (
|
||||
workspaceId: string,
|
||||
memberId: string
|
||||
@@ -76,7 +69,6 @@ export const removeMember = async (
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 更新成员角色
|
||||
export const updateMemberRole = async (
|
||||
workspaceId: string,
|
||||
memberId: string,
|
||||
@@ -88,7 +80,6 @@ export const updateMemberRole = async (
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 离开工作空间
|
||||
export const leaveWorkspace = async (workspaceId: string): Promise<{ message: string }> => {
|
||||
const response = await apiClient.post(`/workspaces/${workspaceId}/leave`);
|
||||
return response.data;
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Alert, Button, Card, Col, Empty, Form, Input, Modal, Row, Select, Space, Table, Tag, Upload, message } from 'antd';
|
||||
import { InboxOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { createAssetLibrary, getAssetLibraries, getAssets, getIngestJob, uploadAsset } from '@/api/assets';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
|
||||
const ProjectAssets: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const projectId = id || '';
|
||||
const [libraryId, setLibraryId] = useState<string>('');
|
||||
const [workspaceId, setWorkspaceId] = useState('demo-workspace');
|
||||
const [createLibraryOpen, setCreateLibraryOpen] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [jobId, setJobId] = useState<string>('');
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const librariesQuery = useQuery({
|
||||
queryKey: ['asset-libraries', projectId],
|
||||
queryFn: () => getAssetLibraries(projectId),
|
||||
enabled: !!projectId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!libraryId && librariesQuery.data?.length) {
|
||||
setLibraryId(librariesQuery.data[0].id);
|
||||
}
|
||||
}, [librariesQuery.data, libraryId]);
|
||||
|
||||
const assetsQuery = useQuery({
|
||||
queryKey: ['assets', libraryId],
|
||||
queryFn: () => getAssets(libraryId),
|
||||
enabled: !!libraryId,
|
||||
});
|
||||
|
||||
const ingestJobQuery = useQuery({
|
||||
queryKey: ['ingest-job', jobId],
|
||||
queryFn: () => getIngestJob(jobId),
|
||||
enabled: !!jobId,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.status;
|
||||
return status === 'completed' || status === 'failed' ? false : 2000;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const status = ingestJobQuery.data?.status;
|
||||
if (status === 'completed') {
|
||||
setUploading(false);
|
||||
assetsQuery.refetch();
|
||||
message.success('素材导入完成');
|
||||
}
|
||||
if (status === 'failed') {
|
||||
setUploading(false);
|
||||
message.error(ingestJobQuery.data?.error_message || '素材导入失败');
|
||||
}
|
||||
}, [ingestJobQuery.data?.status]);
|
||||
|
||||
const createLibraryMutation = useMutation({
|
||||
mutationFn: createAssetLibrary,
|
||||
onSuccess: (library) => {
|
||||
message.success('素材库创建成功');
|
||||
setCreateLibraryOpen(false);
|
||||
librariesQuery.refetch();
|
||||
setLibraryId(library.id);
|
||||
form.resetFields();
|
||||
},
|
||||
onError: () => message.error('素材库创建失败'),
|
||||
});
|
||||
|
||||
const libraryOptions = useMemo(
|
||||
() => (librariesQuery.data || []).map((item) => ({ label: `${item.name} (${item.kind})`, value: item.id })),
|
||||
[librariesQuery.data]
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '类型', dataIndex: 'mime_type', key: 'mime_type', render: (v: string) => <Tag>{v}</Tag> },
|
||||
{ title: '存储键', dataIndex: 'storage_key', key: 'storage_key', ellipsis: true },
|
||||
];
|
||||
|
||||
const customUpload = async (options: any) => {
|
||||
const { file, onSuccess, onError } = options;
|
||||
if (!libraryId) {
|
||||
message.warning('请先选择或创建素材库');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('workspace_id', workspaceId);
|
||||
formData.append('project_id', projectId);
|
||||
formData.append('library_id', libraryId);
|
||||
const result = await uploadAsset(formData);
|
||||
setJobId(result.ingest_job_id);
|
||||
onSuccess(result);
|
||||
message.info('文件已上传,正在导入处理中');
|
||||
} catch (error) {
|
||||
setUploading(false);
|
||||
onError(error);
|
||||
message.error('上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<Card title="项目素材管理" extra={<Space>
|
||||
<Select
|
||||
style={{ width: 280 }}
|
||||
placeholder="选择素材库"
|
||||
value={libraryId || undefined}
|
||||
options={libraryOptions}
|
||||
onChange={setLibraryId}
|
||||
loading={librariesQuery.isLoading}
|
||||
/>
|
||||
<Button icon={<PlusOutlined />} onClick={() => setCreateLibraryOpen(true)}>新建素材库</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { librariesQuery.refetch(); assetsQuery.refetch(); }}>刷新</Button>
|
||||
</Space>}>
|
||||
{ingestJobQuery.data && (
|
||||
<Alert
|
||||
style={{ marginBottom: 16 }}
|
||||
type={ingestJobQuery.data.status === 'failed' ? 'error' : 'info'}
|
||||
message={`导入任务状态:${ingestJobQuery.data.status}`}
|
||||
description={ingestJobQuery.data.error_message || `任务 ID: ${ingestJobQuery.data.id}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dragger
|
||||
name="file"
|
||||
multiple={false}
|
||||
customRequest={customUpload}
|
||||
showUploadList={false}
|
||||
disabled={uploading}
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或拖拽文件到这里上传素材</p>
|
||||
<p className="ant-upload-hint">支持视频、音频、图片文件;上传后自动进入导入任务</p>
|
||||
</Dragger>
|
||||
|
||||
{assetsQuery.data?.length ? (
|
||||
<Table rowKey="id" columns={columns} dataSource={assetsQuery.data} pagination={false} />
|
||||
) : (
|
||||
<Empty description="当前素材库还没有素材" />
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Modal
|
||||
title="新建素材库"
|
||||
open={createLibraryOpen}
|
||||
onCancel={() => setCreateLibraryOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
confirmLoading={createLibraryMutation.isPending}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={(values) => {
|
||||
createLibraryMutation.mutate({
|
||||
workspace_id: workspaceId,
|
||||
project_id: projectId,
|
||||
name: values.name,
|
||||
kind: values.kind,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Form.Item label="Workspace ID" name="workspaceIdHint">
|
||||
<Input value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} />
|
||||
</Form.Item>
|
||||
<Form.Item label="素材库名称" name="name" rules={[{ required: true, message: '请输入素材库名称' }]}>
|
||||
<Input placeholder="例如:项目视频库" />
|
||||
</Form.Item>
|
||||
<Form.Item label="素材库类型" name="kind" initialValue="video" rules={[{ required: true }]}>
|
||||
<Select options={[{ label: '视频库', value: 'video' }, { label: '配音库', value: 'voice' }]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectAssets;
|
||||
export const Component = ProjectAssets;
|
||||
@@ -2,7 +2,7 @@
|
||||
* 工作空间详情页面
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Tabs, Button, Descriptions, Progress, Card, Row, Col } from 'antd';
|
||||
import { PlusOutlined, TeamOutlined, ProjectOutlined } from '@ant-design/icons';
|
||||
import { useWorkspace } from '@/hooks/useWorkspace';
|
||||
@@ -14,6 +14,7 @@ import PermissionMatrix from '@/components/business/PermissionMatrix';
|
||||
|
||||
const WorkspaceDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { data: workspace, isLoading } = useWorkspace(id!);
|
||||
const { data: quota } = useQuery({
|
||||
queryKey: ['quota', id],
|
||||
@@ -41,15 +42,19 @@ const WorkspaceDetail: React.FC = () => {
|
||||
label: '概览',
|
||||
children: (
|
||||
<div>
|
||||
<Card title="工作空间信息" style={{ marginBottom: 16 }}>
|
||||
<Card
|
||||
title="工作空间信息"
|
||||
style={{ marginBottom: 16 }}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate(`/projects/${id}/assets`)}>
|
||||
进入素材管理
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Descriptions column={2}>
|
||||
<Descriptions.Item label="名称">{workspace.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="订阅计划">
|
||||
{workspace.subscription_plan}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{workspace.subscription_status}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="订阅计划">{workspace.subscription_plan}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{workspace.subscription_status}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{new Date(workspace.created_at).toLocaleDateString()}
|
||||
</Descriptions.Item>
|
||||
@@ -62,13 +67,10 @@ const WorkspaceDetail: React.FC = () => {
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<ProjectOutlined /> 项目数量:{quota.projects.used} /{' '}
|
||||
{quota.projects.limit}
|
||||
<ProjectOutlined /> 项目数量:{quota.projects.used} / {quota.projects.limit}
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(
|
||||
(quota.projects.used / quota.projects.limit) * 100
|
||||
)}
|
||||
percent={Math.round((quota.projects.used / quota.projects.limit) * 100)}
|
||||
strokeColor={getStatusColor(quota.projects.status)}
|
||||
/>
|
||||
</div>
|
||||
@@ -76,13 +78,10 @@ const WorkspaceDetail: React.FC = () => {
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
存储空间:{quota.storage.used_gb.toFixed(2)} GB /{' '}
|
||||
{quota.storage.limit_gb} GB
|
||||
存储空间:{quota.storage.used_gb.toFixed(2)} GB / {quota.storage.limit_gb} GB
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(
|
||||
(quota.storage.used_gb / quota.storage.limit_gb) * 100
|
||||
)}
|
||||
percent={Math.round((quota.storage.used_gb / quota.storage.limit_gb) * 100)}
|
||||
strokeColor={getStatusColor(quota.storage.status)}
|
||||
/>
|
||||
</div>
|
||||
@@ -139,5 +138,4 @@ const WorkspaceDetail: React.FC = () => {
|
||||
};
|
||||
|
||||
export default WorkspaceDetail;
|
||||
|
||||
export const Component = WorkspaceDetail;
|
||||
|
||||
@@ -58,6 +58,10 @@ export const router = createBrowserRouter([
|
||||
path: 'workspaces/:id',
|
||||
lazy: () => import('@/pages/workspace/WorkspaceDetail'),
|
||||
},
|
||||
{
|
||||
path: 'projects/:id/assets',
|
||||
lazy: () => import('@/pages/workspace/ProjectAssets'),
|
||||
},
|
||||
{
|
||||
path: 'subscription',
|
||||
lazy: () => import('@/pages/subscription/Plans'),
|
||||
|
||||
@@ -1,6 +1,75 @@
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
|
||||
from .celery_app import celery_app
|
||||
from packages.adapters.sqlalchemy_impl.session import SessionLocal
|
||||
from packages.adapters.sqlalchemy_impl.ingest_job_repository import SQLAlchemyIngestJobRepository
|
||||
from packages.adapters.sqlalchemy_impl.asset_repository import SQLAlchemyAssetRepository
|
||||
from packages.domain import Asset, IngestJobStatus
|
||||
|
||||
|
||||
@celery_app.task(name="worker.healthcheck")
|
||||
def healthcheck() -> dict:
|
||||
return {"ok": True, "service": "worker"}
|
||||
|
||||
|
||||
@celery_app.task(name="worker.ingest_asset")
|
||||
def ingest_asset(job_id: str) -> dict:
|
||||
session = SessionLocal()
|
||||
try:
|
||||
ingest_repo = SQLAlchemyIngestJobRepository(session)
|
||||
asset_repo = SQLAlchemyAssetRepository(session)
|
||||
|
||||
job = ingest_repo.get(job_id)
|
||||
if job is None:
|
||||
return {"ok": False, "error": f"job {job_id} not found"}
|
||||
|
||||
job.status = IngestJobStatus.PROCESSING
|
||||
job.updated_at = datetime.now(timezone.utc)
|
||||
ingest_repo.update(job)
|
||||
|
||||
storage_key = job.storage_key
|
||||
filename = storage_key.split("/")[-1]
|
||||
lower_name = filename.lower()
|
||||
if lower_name.endswith((".mp4", ".mov", ".avi", ".mkv")):
|
||||
mime_type = "video/mp4"
|
||||
elif lower_name.endswith((".mp3", ".wav", ".aac")):
|
||||
mime_type = "audio/mpeg"
|
||||
elif lower_name.endswith((".jpg", ".jpeg")):
|
||||
mime_type = "image/jpeg"
|
||||
elif lower_name.endswith((".png",)):
|
||||
mime_type = "image/png"
|
||||
else:
|
||||
mime_type = "application/octet-stream"
|
||||
|
||||
asset = Asset.create(
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
library_id=job.library_id,
|
||||
name=filename,
|
||||
storage_key=storage_key,
|
||||
mime_type=mime_type,
|
||||
metadata={"source": "ingest_task"},
|
||||
)
|
||||
asset_repo.create(asset)
|
||||
|
||||
job.status = IngestJobStatus.COMPLETED
|
||||
job.result_asset_id = asset.id
|
||||
job.updated_at = datetime.now(timezone.utc)
|
||||
ingest_repo.update(job)
|
||||
|
||||
return {"ok": True, "job_id": job.id, "asset_id": asset.id}
|
||||
except Exception as e:
|
||||
try:
|
||||
ingest_repo = SQLAlchemyIngestJobRepository(session)
|
||||
job = ingest_repo.get(job_id)
|
||||
if job is not None:
|
||||
job.status = IngestJobStatus.FAILED
|
||||
job.error_message = str(e)
|
||||
job.updated_at = datetime.now(timezone.utc)
|
||||
ingest_repo.update(job)
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": False, "job_id": job_id, "error": str(e)}
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
Reference in New Issue
Block a user