This commit is contained in:
Daniil
2026-04-07 13:42:45 +03:00
parent 7d2f444e1c
commit 259d3da89f
34 changed files with 2130 additions and 788 deletions
+2 -2
View File
@@ -173,11 +173,11 @@ async def get_task_status(
if job is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Job not found"
status_code=status.HTTP_404_NOT_FOUND, detail="Задача не найдена"
)
if not current_user.is_staff and job.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Доступ запрещён")
return TaskStatusResponse(
job_id=job.id,
+192 -12
View File
@@ -96,6 +96,10 @@ MESSAGE_COMPLETED = "Завершено"
MESSAGE_PROBING_MEDIA = "Анализ медиафайла"
MESSAGE_PROCESSING = "Обработка"
MESSAGE_CONVERTING = "Конвертация"
MESSAGE_PREPARING_FILE = "Подготовка файла"
MESSAGE_CONVERTING_VIDEO = "Конвертация видео"
MESSAGE_UPLOADING_RESULT = "Загрузка результата"
MESSAGE_SAVING_RESULT = "Сохранение результата"
MESSAGE_RENDERING_CAPTIONS = "Рендеринг субтитров"
MESSAGE_CANCELLED = "Отменено пользователем"
MESSAGE_EXTRACTING_FRAMES = "Извлечение кадров"
@@ -106,6 +110,14 @@ PROGRESS_COMPLETE = 100.0
PROGRESS_MEDIA_PROBE = 50.0
PROGRESS_SILENCE_REMOVE = 30.0
PROGRESS_MEDIA_CONVERT = 30.0
PROGRESS_MEDIA_CONVERT_PREPARING = 5.0
PROGRESS_MEDIA_CONVERT_START = 10.0
PROGRESS_MEDIA_CONVERT_END = 95.0
PROGRESS_MEDIA_CONVERT_SAVING = 99.0
PROGRESS_SILENCE_APPLY_PREPARING = 5.0
PROGRESS_SILENCE_APPLY_START = 10.0
PROGRESS_SILENCE_APPLY_END = 95.0
PROGRESS_SILENCE_APPLY_SAVING = 99.0
PROGRESS_TRANSCRIPTION_START = 20.0
PROGRESS_TRANSCRIPTION_END = 95.0
PROGRESS_CAPTIONS = 30.0
@@ -119,6 +131,7 @@ MESSAGE_DETECTING_SILENCE = "Обнаружение тишины"
MESSAGE_APPLYING_CUTS = "Применение вырезок"
PROGRESS_THROTTLE_SECONDS = 3.0
PROGRESS_CONVERT_THROTTLE_SECONDS = 1.0
ACTIVE_JOB_STATUSES = (JOB_STATUS_PENDING, JOB_STATUS_RUNNING)
DRAMATIQ_BROKER_REF_SEPARATOR = ":"
@@ -481,20 +494,56 @@ def silence_apply_actor(
webhook_url,
TaskWebhookEvent(
status=JOB_STATUS_RUNNING,
current_message=MESSAGE_STARTING,
current_message=MESSAGE_PREPARING_FILE,
progress_pct=PROGRESS_SILENCE_APPLY_PREPARING,
started_at=_utc_now(),
),
)
try:
storage = _get_storage_service()
_send_webhook_event(
webhook_url,
TaskWebhookEvent(
current_message=MESSAGE_APPLYING_CUTS,
progress_pct=PROGRESS_SILENCE_APPLY,
),
)
last_report_time = 0.0
last_progress = PROGRESS_SILENCE_APPLY_PREPARING
def _emit_silence_apply_progress(stage: str, pct: float | None) -> None:
nonlocal last_report_time, last_progress
if stage == "applying_cuts":
raw_pct = min(max(pct or 0.0, 0.0), 100.0)
if raw_pct >= 100.0:
return
mapped = PROGRESS_SILENCE_APPLY_START + (raw_pct / 100.0) * (
PROGRESS_SILENCE_APPLY_END - PROGRESS_SILENCE_APPLY_START
)
message = MESSAGE_APPLYING_CUTS
force = raw_pct == 0.0
elif stage == "uploading":
mapped = PROGRESS_SILENCE_APPLY_END
message = MESSAGE_UPLOADING_RESULT
force = True
else:
return
mapped = round(mapped, 1)
now = time.monotonic()
if not force:
if mapped <= last_progress:
return
if mapped - last_progress < 1.0:
return
if now - last_report_time < PROGRESS_CONVERT_THROTTLE_SECONDS:
return
last_report_time = now
last_progress = max(last_progress, mapped)
_send_webhook_event(
webhook_url,
TaskWebhookEvent(
current_message=message,
progress_pct=last_progress,
),
)
result = _run_async(
apply_silence_cuts(
storage,
@@ -502,8 +551,16 @@ def silence_apply_actor(
out_folder=out_folder,
cuts=cuts,
output_name=output_name,
on_progress=_emit_silence_apply_progress,
)
)
_send_webhook_event(
webhook_url,
TaskWebhookEvent(
current_message=MESSAGE_SAVING_RESULT,
progress_pct=PROGRESS_SILENCE_APPLY_SAVING,
),
)
_send_webhook_event(
webhook_url,
TaskWebhookEvent(
@@ -554,7 +611,8 @@ def media_convert_actor(
webhook_url,
TaskWebhookEvent(
status=JOB_STATUS_RUNNING,
current_message=MESSAGE_STARTING,
current_message=MESSAGE_PREPARING_FILE,
progress_pct=PROGRESS_MEDIA_CONVERT_PREPARING,
started_at=_utc_now(),
),
)
@@ -564,14 +622,63 @@ def media_convert_actor(
raise ValueError(f"Неподдерживаемый формат: {output_format}")
storage = _get_storage_service()
last_report_time = 0.0
last_progress = PROGRESS_MEDIA_CONVERT_PREPARING
def _emit_convert_progress(stage: str, pct: float | None) -> None:
nonlocal last_report_time, last_progress
if stage == "converting":
raw_pct = min(max(pct or 0.0, 0.0), 100.0)
if raw_pct >= 100.0:
return
mapped = PROGRESS_MEDIA_CONVERT_START + (raw_pct / 100.0) * (
PROGRESS_MEDIA_CONVERT_END - PROGRESS_MEDIA_CONVERT_START
)
message = MESSAGE_CONVERTING_VIDEO
force = raw_pct == 0.0
elif stage == "uploading":
mapped = PROGRESS_MEDIA_CONVERT_END
message = MESSAGE_UPLOADING_RESULT
force = True
else:
return
mapped = round(mapped, 1)
now = time.monotonic()
if not force:
if mapped <= last_progress:
return
if mapped - last_progress < 1.0:
return
if now - last_report_time < PROGRESS_CONVERT_THROTTLE_SECONDS:
return
last_report_time = now
last_progress = max(last_progress, mapped)
_send_webhook_event(
webhook_url,
TaskWebhookEvent(
current_message=message,
progress_pct=last_progress,
),
)
result = _run_async(
convert_to_mp4(
storage,
file_key=file_key,
out_folder=out_folder,
on_progress=_emit_convert_progress,
)
)
_send_webhook_event(
webhook_url,
TaskWebhookEvent(
current_message=MESSAGE_CONVERTING,
progress_pct=PROGRESS_MEDIA_CONVERT,
current_message=MESSAGE_SAVING_RESULT,
progress_pct=PROGRESS_MEDIA_CONVERT_SAVING,
),
)
result = _run_async(convert_to_mp4(storage, file_key=file_key, out_folder=out_folder))
_send_webhook_event(
webhook_url,
TaskWebhookEvent(
@@ -1213,6 +1320,12 @@ class TaskService:
except Exception:
logger.exception("Failed to save convert artifacts for job %s", job_id)
if job.job_type == JOB_TYPE_SILENCE_APPLY and event.status == JOB_STATUS_DONE:
try:
await self._save_silence_apply_artifacts(job)
except Exception:
logger.exception("Failed to save silence apply artifacts for job %s", job_id)
if job.job_type == JOB_TYPE_CAPTIONS_GENERATE and event.status == JOB_STATUS_DONE:
try:
await self._save_captions_artifacts(job)
@@ -1427,8 +1540,75 @@ class TaskService:
),
)
updated_output = dict(output_data)
updated_output["file_id"] = str(converted_file.id)
await self._job_repo.update(job, JobUpdate(output_data=updated_output))
logger.info("Saved convert artifacts for job %s", job.id)
async def _save_silence_apply_artifacts(self, job: Job) -> None:
"""Create File and ArtifactMediaFile records for silence-applied video."""
input_data = job.input_data or {}
output_data = job.output_data or {}
file_key: str = input_data["file_key"]
project_id: uuid.UUID | None = (
uuid.UUID(input_data["project_id"]) if input_data.get("project_id") else None
)
file_path: str = output_data["file_path"]
file_size: int = output_data.get("file_size", 0)
user_repo = UserRepository(self._session)
user = await user_repo.get_by_id(job.user_id) # type: ignore[arg-type]
if user is None:
logger.warning(
"User %s not found, skipping silence apply artifact save", job.user_id
)
return
file_repo = FileRepository(self._session)
source_file = await file_repo.get_by_path(file_key)
if source_file is not None:
stem = Path(source_file.original_filename).stem
else:
stem = Path(file_key).stem
processed_filename = f"Видео без тишины {stem}.mp4"
processed_file = await file_repo.create(
requester=user,
data=FileCreate(
project_id=project_id,
original_filename=processed_filename,
path=file_path,
storage_backend="S3",
mime_type="video/mp4",
size_bytes=file_size,
file_format="mp4",
is_uploaded=True,
),
)
artifact_repo = ArtifactRepository(self._session)
await artifact_repo.create(
data=ArtifactMediaFileCreate(
project_id=project_id,
file_id=processed_file.id,
media_file_id=None,
artifact_type="SILENCE_REMOVED_VIDEO",
),
)
updated_output = dict(output_data)
updated_output["file_id"] = str(processed_file.id)
await self._job_repo.update(job, JobUpdate(output_data=updated_output))
logger.info(
"Saved silence apply artifacts for job %s (file_id=%s)",
job.id,
processed_file.id,
)
async def _save_captions_artifacts(self, job: Job) -> None:
"""Create File and ArtifactMediaFile records for captioned video."""
input_data = job.input_data or {}