332 lines
11 KiB
Python
332 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from cpv3.modules.project_workspaces.schemas import (
|
|
ActiveJobState,
|
|
ProjectWorkspaceState,
|
|
SetSourceFileAction,
|
|
SilenceSettingsState,
|
|
SilenceState,
|
|
StartSilenceDetectAction,
|
|
StartTranscriptionAction,
|
|
TranscriptionRequestState,
|
|
)
|
|
from cpv3.modules.project_workspaces.service import (
|
|
ProjectWorkspaceService,
|
|
WorkspaceRevisionConflictError,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_workspace_returns_default_state_when_workspace_missing() -> None:
|
|
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
|
|
|
|
service = ProjectWorkspaceService(session=AsyncMock())
|
|
service._repo = SimpleNamespace(
|
|
get_by_project_id=AsyncMock(return_value=None),
|
|
create=AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
project_id=project.id,
|
|
revision=0,
|
|
state=ProjectWorkspaceState().model_dump(mode="json"),
|
|
)
|
|
),
|
|
)
|
|
|
|
workspace = await service.get_workspace(project=project)
|
|
|
|
assert workspace.revision == 0
|
|
assert workspace.phase == "INGEST"
|
|
assert workspace.current_screen == "upload"
|
|
assert workspace.active_job is None
|
|
assert workspace.source_file_id is None
|
|
assert workspace.workspace_view.used_file_ids == []
|
|
assert workspace.workspace_view.selected_file_id is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_action_set_source_file_moves_workspace_to_verify() -> None:
|
|
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
|
|
requester = SimpleNamespace(id=uuid.uuid4(), is_staff=False)
|
|
file_id = uuid.uuid4()
|
|
workspace_row = SimpleNamespace(
|
|
project_id=project.id,
|
|
revision=0,
|
|
state=ProjectWorkspaceState().model_dump(mode="json"),
|
|
)
|
|
saved_state: dict[str, object] = {}
|
|
|
|
async def update_state(*, project_id, expected_revision, state):
|
|
saved_state.update(state)
|
|
return SimpleNamespace(
|
|
project_id=project_id,
|
|
revision=expected_revision + 1,
|
|
state=state,
|
|
)
|
|
|
|
service = ProjectWorkspaceService(session=AsyncMock())
|
|
service._repo = SimpleNamespace(
|
|
get_by_project_id=AsyncMock(return_value=workspace_row),
|
|
create=AsyncMock(),
|
|
update_state=AsyncMock(side_effect=update_state),
|
|
)
|
|
service._file_repo = SimpleNamespace(
|
|
get_by_id=AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
id=file_id,
|
|
owner_id=requester.id,
|
|
project_id=project.id,
|
|
path="users/test/source.mp4",
|
|
)
|
|
)
|
|
)
|
|
|
|
workspace = await service.apply_action(
|
|
requester=requester,
|
|
project=project,
|
|
action=SetSourceFileAction(
|
|
type="SET_SOURCE_FILE",
|
|
revision=0,
|
|
file_id=file_id,
|
|
),
|
|
)
|
|
|
|
assert workspace.revision == 1
|
|
assert workspace.phase == "VERIFY"
|
|
assert workspace.current_screen == "verify"
|
|
assert workspace.source_file_id == file_id
|
|
assert saved_state["source_file_id"] == str(file_id)
|
|
assert saved_state["workspace_view"] == {
|
|
"used_file_ids": [str(file_id)],
|
|
"selected_file_id": str(file_id),
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_action_rejects_stale_revision() -> None:
|
|
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
|
|
workspace_row = SimpleNamespace(
|
|
project_id=project.id,
|
|
revision=2,
|
|
state=ProjectWorkspaceState().model_dump(mode="json"),
|
|
)
|
|
|
|
service = ProjectWorkspaceService(session=AsyncMock())
|
|
service._repo = SimpleNamespace(
|
|
get_by_project_id=AsyncMock(return_value=workspace_row),
|
|
create=AsyncMock(),
|
|
update_state=AsyncMock(),
|
|
)
|
|
|
|
with pytest.raises(WorkspaceRevisionConflictError):
|
|
await service.apply_action(
|
|
requester=SimpleNamespace(id=uuid.uuid4(), is_staff=False),
|
|
project=project,
|
|
action=SetSourceFileAction(
|
|
type="SET_SOURCE_FILE",
|
|
revision=1,
|
|
file_id=uuid.uuid4(),
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_silence_detect_submits_task_and_tracks_active_job() -> None:
|
|
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
|
|
requester = SimpleNamespace(id=uuid.uuid4(), is_staff=False)
|
|
source_file_id = uuid.uuid4()
|
|
workspace_state = ProjectWorkspaceState(
|
|
phase="SILENCE",
|
|
source_file_id=source_file_id,
|
|
silence=SilenceState(
|
|
status="CONFIGURED",
|
|
settings=SilenceSettingsState(
|
|
min_silence_duration_ms=250,
|
|
silence_threshold_db=18,
|
|
padding_ms=125,
|
|
),
|
|
),
|
|
)
|
|
workspace_row = SimpleNamespace(
|
|
project_id=project.id,
|
|
revision=0,
|
|
state=workspace_state.model_dump(mode="json"),
|
|
)
|
|
submitted_response = SimpleNamespace(job_id=uuid.uuid4(), status="PENDING")
|
|
|
|
async def update_state(*, project_id, expected_revision, state):
|
|
return SimpleNamespace(
|
|
project_id=project_id,
|
|
revision=expected_revision + 1,
|
|
state=state,
|
|
)
|
|
|
|
task_service = SimpleNamespace(
|
|
submit_silence_detect=AsyncMock(return_value=submitted_response),
|
|
)
|
|
|
|
service = ProjectWorkspaceService(session=AsyncMock())
|
|
service._repo = SimpleNamespace(
|
|
get_by_project_id=AsyncMock(return_value=workspace_row),
|
|
create=AsyncMock(),
|
|
update_state=AsyncMock(side_effect=update_state),
|
|
)
|
|
service._file_repo = SimpleNamespace(
|
|
get_by_id=AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
id=source_file_id,
|
|
owner_id=requester.id,
|
|
project_id=project.id,
|
|
path="projects/test/video.mp4",
|
|
)
|
|
)
|
|
)
|
|
service._task_service_factory = lambda: task_service
|
|
|
|
workspace = await service.apply_action(
|
|
requester=requester,
|
|
project=project,
|
|
action=StartSilenceDetectAction(
|
|
type="START_SILENCE_DETECT",
|
|
revision=0,
|
|
),
|
|
)
|
|
|
|
task_service.submit_silence_detect.assert_awaited_once()
|
|
assert workspace.current_screen == "processing"
|
|
assert workspace.active_job == ActiveJobState(
|
|
job_id=submitted_response.job_id,
|
|
job_type="SILENCE_DETECT",
|
|
)
|
|
assert workspace.silence.detect_job_id == submitted_response.job_id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_transcription_persists_request_and_processing_job() -> None:
|
|
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
|
|
requester = SimpleNamespace(id=uuid.uuid4(), is_staff=False)
|
|
source_file_id = uuid.uuid4()
|
|
workspace_state = ProjectWorkspaceState(
|
|
phase="TRANSCRIPTION",
|
|
source_file_id=source_file_id,
|
|
)
|
|
workspace_row = SimpleNamespace(
|
|
project_id=project.id,
|
|
revision=3,
|
|
state=workspace_state.model_dump(mode="json"),
|
|
)
|
|
submitted_response = SimpleNamespace(job_id=uuid.uuid4(), status="PENDING")
|
|
|
|
async def update_state(*, project_id, expected_revision, state):
|
|
return SimpleNamespace(
|
|
project_id=project_id,
|
|
revision=expected_revision + 1,
|
|
state=state,
|
|
)
|
|
|
|
task_service = SimpleNamespace(
|
|
submit_transcription_generate=AsyncMock(return_value=submitted_response),
|
|
)
|
|
|
|
service = ProjectWorkspaceService(session=AsyncMock())
|
|
service._repo = SimpleNamespace(
|
|
get_by_project_id=AsyncMock(return_value=workspace_row),
|
|
create=AsyncMock(),
|
|
update_state=AsyncMock(side_effect=update_state),
|
|
)
|
|
service._file_repo = SimpleNamespace(
|
|
get_by_id=AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
id=source_file_id,
|
|
owner_id=requester.id,
|
|
project_id=project.id,
|
|
path="projects/test/video.mp4",
|
|
)
|
|
)
|
|
)
|
|
service._task_service_factory = lambda: task_service
|
|
|
|
request = TranscriptionRequestState(engine="whisper", language="ru", model="base")
|
|
workspace = await service.apply_action(
|
|
requester=requester,
|
|
project=project,
|
|
action=StartTranscriptionAction(
|
|
type="START_TRANSCRIPTION",
|
|
revision=3,
|
|
request=request,
|
|
),
|
|
)
|
|
|
|
assert workspace.current_screen == "transcription-processing"
|
|
assert workspace.transcription.request == request
|
|
assert workspace.active_job == ActiveJobState(
|
|
job_id=submitted_response.job_id,
|
|
job_type="TRANSCRIPTION_GENERATE",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_job_update_moves_transcription_job_to_review() -> None:
|
|
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
|
|
job_id = uuid.uuid4()
|
|
transcription_id = uuid.uuid4()
|
|
artifact_id = uuid.uuid4()
|
|
workspace_state = ProjectWorkspaceState(
|
|
phase="TRANSCRIPTION",
|
|
active_job=ActiveJobState(job_id=job_id, job_type="TRANSCRIPTION_GENERATE"),
|
|
transcription={
|
|
"status": "PROCESSING",
|
|
"job_id": job_id,
|
|
"artifact_id": None,
|
|
"transcription_id": None,
|
|
"reviewed": False,
|
|
},
|
|
)
|
|
workspace_row = SimpleNamespace(
|
|
project_id=project.id,
|
|
revision=4,
|
|
state=workspace_state.model_dump(mode="json"),
|
|
)
|
|
|
|
async def update_state(*, project_id, expected_revision, state):
|
|
return SimpleNamespace(
|
|
project_id=project_id,
|
|
revision=expected_revision + 1,
|
|
state=state,
|
|
)
|
|
|
|
service = ProjectWorkspaceService(session=AsyncMock())
|
|
service._repo = SimpleNamespace(
|
|
get_by_project_id=AsyncMock(return_value=workspace_row),
|
|
create=AsyncMock(),
|
|
update_state=AsyncMock(side_effect=update_state),
|
|
)
|
|
|
|
workspace = await service.apply_job_update(
|
|
project=project,
|
|
job=SimpleNamespace(
|
|
id=job_id,
|
|
project_id=project.id,
|
|
job_type="TRANSCRIPTION_GENERATE",
|
|
status="DONE",
|
|
output_data={
|
|
"transcription_id": str(transcription_id),
|
|
"artifact_id": str(artifact_id),
|
|
},
|
|
),
|
|
)
|
|
|
|
assert workspace is not None
|
|
assert workspace.revision == 5
|
|
assert workspace.phase == "TRANSCRIPTION"
|
|
assert workspace.current_screen == "subtitle-revision"
|
|
assert workspace.active_job is None
|
|
assert workspace.transcription.transcription_id == transcription_id
|
|
assert workspace.transcription.artifact_id == artifact_id
|
|
assert workspace.transcription.reviewed is False
|