ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: preflight и запись audio для Voice Tasker
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.app.views import (
|
||||
VoiceTaskParseEndpoint,
|
||||
VoiceTaskPreflightEndpoint,
|
||||
WorkspaceAISettingsEndpoint,
|
||||
WorkspaceAISettingsTestConnectionEndpoint,
|
||||
)
|
||||
@@ -21,4 +23,14 @@ urlpatterns = [
|
||||
WorkspaceAISettingsTestConnectionEndpoint.as_view(),
|
||||
name="voice-tasker-settings-test-connection",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/voice-task/preflight/",
|
||||
VoiceTaskPreflightEndpoint.as_view(),
|
||||
name="voice-task-preflight",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/voice-task/parse/",
|
||||
VoiceTaskParseEndpoint.as_view(),
|
||||
name="voice-task-parse",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -244,6 +244,8 @@ from .webhook.base import (
|
||||
)
|
||||
|
||||
from .voice_tasker import (
|
||||
VoiceTaskParseEndpoint,
|
||||
VoiceTaskPreflightEndpoint,
|
||||
WorkspaceAISettingsEndpoint,
|
||||
WorkspaceAISettingsTestConnectionEndpoint,
|
||||
)
|
||||
|
||||
@@ -2,19 +2,65 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
import json
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.parsers import FormParser, MultiPartParser
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.app.permissions import ROLE, allow_permission
|
||||
from plane.app.serializers import WorkspaceAISettingsSerializer
|
||||
from plane.db.models import Workspace, WorkspaceAICredential, WorkspaceAISettings
|
||||
from plane.db.models import Workspace, WorkspaceAICredential, WorkspaceAISettings, WorkspaceMember
|
||||
from plane.license.utils.encryption import decrypt_data
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
||||
from .base import BaseAPIView
|
||||
|
||||
VOICE_TASK_ACCEPTED_AUDIO_TYPES = ["audio/webm", "audio/mp4", "audio/mpeg", "audio/wav"]
|
||||
|
||||
|
||||
def get_voice_task_preflight(workspace, user):
|
||||
ai_settings = WorkspaceAISettings.objects.filter(workspace=workspace).first()
|
||||
workspace_member = WorkspaceMember.objects.filter(workspace=workspace, member=user, is_active=True).first()
|
||||
|
||||
response = {
|
||||
"available": False,
|
||||
"reason": "not_configured",
|
||||
"max_audio_duration_seconds": 120,
|
||||
"accepted_mime_types": VOICE_TASK_ACCEPTED_AUDIO_TYPES,
|
||||
"access_mode": "all_workspace_members",
|
||||
}
|
||||
|
||||
if not ai_settings:
|
||||
return response
|
||||
|
||||
response["max_audio_duration_seconds"] = ai_settings.max_audio_duration_seconds
|
||||
response["access_mode"] = ai_settings.access_mode
|
||||
|
||||
if not ai_settings.voice_tasker_enabled:
|
||||
response["reason"] = "disabled"
|
||||
return response
|
||||
|
||||
credential = WorkspaceAICredential.objects.filter(
|
||||
workspace=workspace,
|
||||
provider=ai_settings.provider,
|
||||
is_active=True,
|
||||
).first()
|
||||
if not credential or not credential.encrypted_api_key:
|
||||
response["reason"] = "missing_api_key"
|
||||
return response
|
||||
|
||||
if ai_settings.access_mode == WorkspaceAISettings.AccessMode.ADMINS_ONLY:
|
||||
if not workspace_member or workspace_member.role != ROLE.ADMIN.value:
|
||||
response["reason"] = "role_denied"
|
||||
return response
|
||||
|
||||
response["available"] = True
|
||||
response["reason"] = None
|
||||
return response
|
||||
|
||||
|
||||
class WorkspaceAISettingsEndpoint(BaseAPIView):
|
||||
def get_settings(self, slug):
|
||||
@@ -105,3 +151,81 @@ class WorkspaceAISettingsTestConnectionEndpoint(BaseAPIView):
|
||||
},
|
||||
status=status_code,
|
||||
)
|
||||
|
||||
|
||||
class VoiceTaskPreflightEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
return Response(get_voice_task_preflight(workspace, request.user), status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class VoiceTaskParseEndpoint(BaseAPIView):
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def post(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
preflight = get_voice_task_preflight(workspace, request.user)
|
||||
|
||||
if not preflight["available"]:
|
||||
response_status = status.HTTP_403_FORBIDDEN if preflight["reason"] == "role_denied" else status.HTTP_400_BAD_REQUEST
|
||||
return Response(
|
||||
{
|
||||
"ok": False,
|
||||
"code": preflight["reason"],
|
||||
"error": "Voice Tasker is not available for this workspace.",
|
||||
},
|
||||
status=response_status,
|
||||
)
|
||||
|
||||
audio = request.FILES.get("audio")
|
||||
if not audio:
|
||||
return Response(
|
||||
{"ok": False, "code": "missing_audio", "error": "Audio file is required."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if audio.content_type not in VOICE_TASK_ACCEPTED_AUDIO_TYPES:
|
||||
return Response(
|
||||
{"ok": False, "code": "unsupported_audio_type", "error": "Unsupported audio file type."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
try:
|
||||
duration_seconds = float(request.data.get("duration_seconds", 0))
|
||||
except (TypeError, ValueError):
|
||||
duration_seconds = 0
|
||||
|
||||
if duration_seconds <= 0:
|
||||
return Response(
|
||||
{"ok": False, "code": "invalid_duration", "error": "Audio duration is required."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if duration_seconds > preflight["max_audio_duration_seconds"]:
|
||||
return Response(
|
||||
{"ok": False, "code": "audio_too_long", "error": "Audio duration exceeds workspace limit."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
client_context_raw = request.data.get("client_context") or "{}"
|
||||
try:
|
||||
client_context = json.loads(client_context_raw)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
client_context = {}
|
||||
|
||||
return Response(
|
||||
{
|
||||
"ok": True,
|
||||
"status": "uploaded",
|
||||
"pipeline_status": "pending_openai_pipeline",
|
||||
"audio": {
|
||||
"content_type": audio.content_type,
|
||||
"duration_seconds": duration_seconds,
|
||||
"size": audio.size,
|
||||
},
|
||||
"client_context": client_context,
|
||||
},
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user