ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: очередь Voice Tasker и concurrency control
This commit is contained in:
@@ -253,6 +253,7 @@ from .voice_tasker import (
|
||||
VoiceTaskCommitEndpoint,
|
||||
VoiceTaskParseEndpoint,
|
||||
VoiceTaskPreflightEndpoint,
|
||||
VoiceTaskSessionEndpoint,
|
||||
WorkspaceAISettingsEndpoint,
|
||||
WorkspaceAISettingsTestConnectionEndpoint,
|
||||
)
|
||||
|
||||
@@ -7,14 +7,17 @@ import json
|
||||
import math
|
||||
import re
|
||||
import uuid
|
||||
import zlib
|
||||
from datetime import date, timedelta
|
||||
from difflib import SequenceMatcher
|
||||
from html import escape
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import Max, Q
|
||||
from django.utils.text import get_valid_filename
|
||||
from django.utils import timezone
|
||||
|
||||
from openai import OpenAI
|
||||
@@ -63,6 +66,8 @@ VOICE_TASK_MEMORY_LIMIT = 5
|
||||
VOICE_TASK_CONTEXT_LIMIT = 100
|
||||
VOICE_TASK_RATE_LIMIT_HOURLY_WINDOW_SECONDS = 60 * 60
|
||||
VOICE_TASK_RATE_LIMIT_DAILY_WINDOW_SECONDS = 24 * 60 * 60
|
||||
VOICE_TASK_CONCURRENCY_RETRY_AFTER_SECONDS = 15
|
||||
VOICE_TASK_ACTIVE_SESSION_STALE_SECONDS = 30 * 60
|
||||
VOICE_TASK_RATE_LIMIT_ERROR_CODES = {
|
||||
"voice_task_user_hourly_limit_exceeded",
|
||||
"voice_task_workspace_hourly_limit_exceeded",
|
||||
@@ -70,6 +75,16 @@ VOICE_TASK_RATE_LIMIT_ERROR_CODES = {
|
||||
"voice_task_workspace_daily_limit_exceeded",
|
||||
"voice_task_project_daily_limit_exceeded",
|
||||
}
|
||||
VOICE_TASK_CONCURRENCY_ERROR_CODES = {"voice_task_workspace_concurrency_limit_exceeded"}
|
||||
VOICE_TASK_LIMIT_EXCLUDED_ERROR_CODES = VOICE_TASK_RATE_LIMIT_ERROR_CODES | VOICE_TASK_CONCURRENCY_ERROR_CODES
|
||||
VOICE_TASK_ACTIVE_SESSION_STATUSES = {
|
||||
VoiceTaskSession.Status.QUEUED,
|
||||
VoiceTaskSession.Status.PROCESSING,
|
||||
VoiceTaskSession.Status.UPLOADED,
|
||||
VoiceTaskSession.Status.TRANSCRIBING,
|
||||
VoiceTaskSession.Status.TRANSCRIBED,
|
||||
VoiceTaskSession.Status.PARSING,
|
||||
}
|
||||
VOICE_TASK_PROJECT_MATCH_THRESHOLD = 0.8
|
||||
VOICE_TASK_ASSIGNEE_MATCH_THRESHOLD = 0.8
|
||||
VOICE_TASK_STATE_MATCH_THRESHOLD = 0.8
|
||||
@@ -345,7 +360,7 @@ def get_voice_task_limit_sessions(workspace, window_seconds, now=None):
|
||||
workspace=workspace,
|
||||
created_at__gte=window_start,
|
||||
created_at__lte=now,
|
||||
).exclude(error_code__in=VOICE_TASK_RATE_LIMIT_ERROR_CODES)
|
||||
).exclude(error_code__in=VOICE_TASK_LIMIT_EXCLUDED_ERROR_CODES)
|
||||
|
||||
|
||||
def get_voice_task_rate_limit_state(workspace, user, project, ai_settings, now=None):
|
||||
@@ -480,6 +495,74 @@ def get_voice_task_rate_limit_error(workspace, user, project, ai_settings, now=N
|
||||
return None
|
||||
|
||||
|
||||
def get_voice_task_workspace_lock_key(workspace_id):
|
||||
return zlib.crc32(f"voice-tasker:{workspace_id}".encode("utf-8"))
|
||||
|
||||
|
||||
def get_voice_task_audio_filename(audio, audio_content_type):
|
||||
original_name = get_valid_filename(getattr(audio, "name", "") or "")
|
||||
if original_name:
|
||||
return f"{uuid.uuid4()}-{original_name}"
|
||||
|
||||
extension = {
|
||||
"audio/webm": "webm",
|
||||
"audio/mp4": "m4a",
|
||||
"audio/mpeg": "mp3",
|
||||
"audio/wav": "wav",
|
||||
}.get(audio_content_type, "webm")
|
||||
return f"{uuid.uuid4()}.{extension}"
|
||||
|
||||
|
||||
def save_voice_task_audio_file(voice_session, audio, audio_content_type):
|
||||
audio.seek(0)
|
||||
voice_session.audio_file.save(
|
||||
get_voice_task_audio_filename(audio, audio_content_type),
|
||||
ContentFile(audio.read()),
|
||||
save=False,
|
||||
)
|
||||
|
||||
|
||||
def clear_voice_task_audio_file(voice_session):
|
||||
if not voice_session.audio_file:
|
||||
return
|
||||
|
||||
voice_session.audio_file.delete(save=False)
|
||||
voice_session.audio_file = None
|
||||
voice_session.save(update_fields=["audio_file", "updated_at"])
|
||||
|
||||
|
||||
def get_voice_task_concurrency_state(workspace, ai_settings):
|
||||
active_sessions = VoiceTaskSession.objects.filter(
|
||||
workspace=workspace,
|
||||
status__in=VOICE_TASK_ACTIVE_SESSION_STATUSES,
|
||||
updated_at__gte=timezone.now() - timedelta(seconds=VOICE_TASK_ACTIVE_SESSION_STALE_SECONDS),
|
||||
).count()
|
||||
concurrency_limit = max(int(ai_settings.workspace_concurrency_limit or 0), 0)
|
||||
|
||||
return {
|
||||
"scope": "workspace",
|
||||
"limit": concurrency_limit,
|
||||
"used": active_sessions,
|
||||
"exceeded": bool(concurrency_limit and active_sessions >= concurrency_limit),
|
||||
"retry_after": VOICE_TASK_CONCURRENCY_RETRY_AFTER_SECONDS,
|
||||
}
|
||||
|
||||
|
||||
def get_voice_task_concurrency_error(workspace, ai_settings):
|
||||
state = get_voice_task_concurrency_state(workspace, ai_settings)
|
||||
if not state["exceeded"]:
|
||||
return None
|
||||
|
||||
return {
|
||||
"code": "voice_task_workspace_concurrency_limit_exceeded",
|
||||
"message": "Voice Tasker processing queue is full for this workspace.",
|
||||
"scope": state["scope"],
|
||||
"limit": state["limit"],
|
||||
"used": state["used"],
|
||||
"retry_after": state["retry_after"],
|
||||
}
|
||||
|
||||
|
||||
def create_voice_task_rate_limit_session(
|
||||
workspace,
|
||||
user,
|
||||
@@ -504,6 +587,96 @@ def create_voice_task_rate_limit_session(
|
||||
)
|
||||
|
||||
|
||||
def create_voice_task_failed_preflight_session(
|
||||
workspace,
|
||||
user,
|
||||
audio,
|
||||
audio_content_type,
|
||||
duration_seconds,
|
||||
client_context,
|
||||
error,
|
||||
project=None,
|
||||
):
|
||||
return VoiceTaskSession.objects.create(
|
||||
workspace=workspace,
|
||||
user=user,
|
||||
project=project,
|
||||
status=VoiceTaskSession.Status.FAILED,
|
||||
audio_duration_seconds=duration_seconds,
|
||||
audio_content_type=audio_content_type,
|
||||
audio_size=getattr(audio, "size", None),
|
||||
client_context=client_context,
|
||||
error_code=error["code"],
|
||||
error_message=error["message"],
|
||||
)
|
||||
|
||||
|
||||
def reserve_voice_task_session(
|
||||
workspace,
|
||||
user,
|
||||
audio,
|
||||
audio_content_type,
|
||||
duration_seconds,
|
||||
client_context,
|
||||
request_project_id=None,
|
||||
):
|
||||
with transaction.atomic():
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [get_voice_task_workspace_lock_key(workspace.id)])
|
||||
|
||||
ai_settings = WorkspaceAISettings.objects.select_for_update().filter(workspace=workspace).first()
|
||||
if not ai_settings:
|
||||
raise VoiceTaskerPipelineError(
|
||||
"not_configured",
|
||||
"Voice Tasker is not configured for this workspace.",
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
quota_project = get_voice_task_quota_project(workspace, project_id=request_project_id, ai_settings=ai_settings)
|
||||
rate_limit_error = get_voice_task_rate_limit_error(workspace, user, quota_project, ai_settings)
|
||||
if rate_limit_error:
|
||||
voice_session = create_voice_task_rate_limit_session(
|
||||
workspace=workspace,
|
||||
user=user,
|
||||
audio=audio,
|
||||
audio_content_type=audio_content_type,
|
||||
duration_seconds=duration_seconds,
|
||||
client_context=client_context,
|
||||
rate_limit_error=rate_limit_error,
|
||||
project=quota_project,
|
||||
)
|
||||
return None, voice_session, rate_limit_error
|
||||
|
||||
concurrency_error = get_voice_task_concurrency_error(workspace, ai_settings)
|
||||
if concurrency_error:
|
||||
voice_session = create_voice_task_failed_preflight_session(
|
||||
workspace=workspace,
|
||||
user=user,
|
||||
audio=audio,
|
||||
audio_content_type=audio_content_type,
|
||||
duration_seconds=duration_seconds,
|
||||
client_context=client_context,
|
||||
error=concurrency_error,
|
||||
project=quota_project,
|
||||
)
|
||||
return None, voice_session, concurrency_error
|
||||
|
||||
voice_session = VoiceTaskSession(
|
||||
workspace=workspace,
|
||||
user=user,
|
||||
project=quota_project,
|
||||
status=VoiceTaskSession.Status.QUEUED,
|
||||
audio_duration_seconds=duration_seconds,
|
||||
audio_content_type=audio_content_type,
|
||||
audio_size=getattr(audio, "size", None),
|
||||
client_context=client_context,
|
||||
)
|
||||
save_voice_task_audio_file(voice_session, audio, audio_content_type)
|
||||
voice_session.save()
|
||||
|
||||
return voice_session, voice_session, None
|
||||
|
||||
|
||||
class VoiceTaskerPipelineError(Exception):
|
||||
def __init__(self, code, message, response_status=status.HTTP_400_BAD_REQUEST):
|
||||
self.code = code
|
||||
@@ -573,10 +746,11 @@ class OpenAITranscriptionService:
|
||||
self.client = OpenAI(api_key=api_key)
|
||||
self.model = model
|
||||
|
||||
def transcribe(self, audio, language=None):
|
||||
def transcribe(self, audio, language=None, content_type=None):
|
||||
audio.seek(0)
|
||||
file_name = audio.name or "voice-task.webm"
|
||||
payload = (file_name, audio.read(), normalize_audio_content_type(audio.content_type) or "audio/webm")
|
||||
normalized_content_type = normalize_audio_content_type(content_type or getattr(audio, "content_type", ""))
|
||||
payload = (file_name, audio.read(), normalized_content_type or "audio/webm")
|
||||
params = {
|
||||
"model": self.model,
|
||||
"file": payload,
|
||||
@@ -2473,6 +2647,167 @@ def voice_task_requires_confirmation(parsed, warnings):
|
||||
)
|
||||
|
||||
|
||||
def process_voice_task_session_pipeline(voice_session_id):
|
||||
voice_session = VoiceTaskSession.objects.select_related("workspace", "user", "project").filter(
|
||||
id=voice_session_id
|
||||
).first()
|
||||
if not voice_session:
|
||||
return
|
||||
|
||||
if voice_session.status == VoiceTaskSession.Status.PARSED:
|
||||
return
|
||||
|
||||
try:
|
||||
if not voice_session.audio_file:
|
||||
raise VoiceTaskerPipelineError(
|
||||
"missing_audio",
|
||||
"Voice Tasker audio file is not available for processing.",
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = voice_session.workspace
|
||||
user = voice_session.user
|
||||
client_context = voice_session.client_context or {}
|
||||
ai_settings, api_key = get_workspace_ai_runtime(workspace)
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.PROCESSING
|
||||
voice_session.save(update_fields=["status", "updated_at"])
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.TRANSCRIBING
|
||||
voice_session.save(update_fields=["status", "updated_at"])
|
||||
|
||||
voice_session.audio_file.open("rb")
|
||||
try:
|
||||
transcript = OpenAITranscriptionService(
|
||||
api_key=api_key,
|
||||
model=ai_settings.transcription_model,
|
||||
).transcribe(
|
||||
voice_session.audio_file,
|
||||
language=get_client_language(client_context),
|
||||
content_type=voice_session.audio_content_type,
|
||||
)
|
||||
finally:
|
||||
voice_session.audio_file.close()
|
||||
|
||||
if not transcript:
|
||||
raise VoiceTaskerPipelineError(
|
||||
"empty_transcript",
|
||||
"OpenAI returned an empty transcript.",
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.TRANSCRIBED
|
||||
voice_session.transcript = transcript
|
||||
voice_session.save(update_fields=["status", "transcript", "updated_at"])
|
||||
|
||||
parser_context = build_voice_task_parser_context(
|
||||
workspace=workspace,
|
||||
user=user,
|
||||
transcript=transcript,
|
||||
client_context=client_context,
|
||||
)
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.PARSING
|
||||
voice_session.save(update_fields=["status", "updated_at"])
|
||||
|
||||
parsed = VoiceTaskParserService(
|
||||
api_key=api_key,
|
||||
model=ai_settings.structuring_model,
|
||||
).parse(parser_context)
|
||||
if not parsed.get("state_hint"):
|
||||
inferred_state_hint = infer_voice_task_state_hint(transcript)
|
||||
if inferred_state_hint:
|
||||
parsed["state_hint"] = inferred_state_hint
|
||||
parsed = harden_voice_task_intent(parsed, transcript)
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.PARSED
|
||||
voice_session.intent = parsed["intent"]
|
||||
voice_session.parsed_json = parsed
|
||||
if not voice_session.project_id and parsed.get("project_id"):
|
||||
voice_session.project = get_voice_task_quota_project(workspace, project_id=parsed.get("project_id"))
|
||||
voice_session.error_code = ""
|
||||
voice_session.error_message = ""
|
||||
voice_session.save(
|
||||
update_fields=["status", "intent", "parsed_json", "project", "error_code", "error_message", "updated_at"]
|
||||
)
|
||||
except VoiceTaskerPipelineError as exc:
|
||||
pipeline_error = exc
|
||||
voice_session.status = VoiceTaskSession.Status.FAILED
|
||||
voice_session.error_code = pipeline_error.code
|
||||
voice_session.error_message = pipeline_error.message
|
||||
voice_session.save(update_fields=["status", "error_code", "error_message", "updated_at"])
|
||||
except Exception as exc:
|
||||
pipeline_error = get_openai_pipeline_error(exc)
|
||||
voice_session.status = VoiceTaskSession.Status.FAILED
|
||||
voice_session.error_code = pipeline_error.code
|
||||
voice_session.error_message = pipeline_error.message
|
||||
voice_session.save(update_fields=["status", "error_code", "error_message", "updated_at"])
|
||||
finally:
|
||||
try:
|
||||
clear_voice_task_audio_file(voice_session)
|
||||
except Exception as exc:
|
||||
log_exception(exc)
|
||||
|
||||
|
||||
def serialize_voice_task_session_response(voice_session):
|
||||
base_payload = {
|
||||
"ok": voice_session.status != VoiceTaskSession.Status.FAILED,
|
||||
"status": voice_session.status,
|
||||
"pipeline_status": voice_session.status,
|
||||
"voice_session_id": str(voice_session.id),
|
||||
"audio": {
|
||||
"content_type": voice_session.audio_content_type,
|
||||
"duration_seconds": voice_session.audio_duration_seconds,
|
||||
"size": voice_session.audio_size,
|
||||
},
|
||||
"client_context": voice_session.client_context,
|
||||
}
|
||||
|
||||
if voice_session.status == VoiceTaskSession.Status.FAILED:
|
||||
base_payload.update(
|
||||
{
|
||||
"code": voice_session.error_code,
|
||||
"error": voice_session.error_message or "Voice Tasker failed to process audio.",
|
||||
}
|
||||
)
|
||||
return base_payload
|
||||
|
||||
if voice_session.status != VoiceTaskSession.Status.PARSED:
|
||||
return base_payload
|
||||
|
||||
workspace = voice_session.workspace
|
||||
ai_settings = WorkspaceAISettings.objects.filter(workspace=workspace).first()
|
||||
parsed = normalize_voice_task_parse(voice_session.parsed_json or {})
|
||||
transcript = voice_session.transcript
|
||||
warnings = get_voice_task_warnings(parsed, transcript)
|
||||
resolution = build_voice_task_resolution(
|
||||
workspace=workspace,
|
||||
user=voice_session.user,
|
||||
ai_settings=ai_settings,
|
||||
draft=parsed,
|
||||
client_context=voice_session.client_context or {},
|
||||
voice_session=voice_session,
|
||||
transcript=transcript,
|
||||
)
|
||||
warnings = list(dict.fromkeys(warnings + resolution["warnings"]))
|
||||
|
||||
base_payload.update(
|
||||
{
|
||||
"transcript": transcript,
|
||||
"intent": parsed["intent"],
|
||||
"draft": parsed,
|
||||
"resolution": resolution,
|
||||
"warnings": warnings,
|
||||
"requires_confirmation": voice_task_requires_confirmation(parsed, warnings),
|
||||
"models": {
|
||||
"transcription": ai_settings.transcription_model if ai_settings else "",
|
||||
"structuring": ai_settings.structuring_model if ai_settings else "",
|
||||
},
|
||||
}
|
||||
)
|
||||
return base_payload
|
||||
|
||||
|
||||
class WorkspaceAISettingsEndpoint(BaseAPIView):
|
||||
def get_settings(self, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
@@ -2637,162 +2972,80 @@ class VoiceTaskParseEndpoint(BaseAPIView):
|
||||
if not isinstance(client_context, dict):
|
||||
client_context = {}
|
||||
|
||||
ai_settings = WorkspaceAISettings.objects.filter(workspace=workspace).first()
|
||||
if not ai_settings:
|
||||
return Response(
|
||||
{"ok": False, "code": "not_configured", "error": "Voice Tasker is not configured for this workspace."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
quota_project = get_voice_task_quota_project(workspace, project_id=request_project_id, ai_settings=ai_settings)
|
||||
rate_limit_error = get_voice_task_rate_limit_error(workspace, request.user, quota_project, ai_settings)
|
||||
if rate_limit_error:
|
||||
voice_session = create_voice_task_rate_limit_session(
|
||||
try:
|
||||
voice_session, recorded_session, preflight_error = reserve_voice_task_session(
|
||||
workspace=workspace,
|
||||
user=request.user,
|
||||
audio=audio,
|
||||
audio_content_type=audio_content_type,
|
||||
duration_seconds=duration_seconds,
|
||||
client_context=client_context,
|
||||
rate_limit_error=rate_limit_error,
|
||||
project=quota_project,
|
||||
request_project_id=request_project_id,
|
||||
)
|
||||
except VoiceTaskerPipelineError as exc:
|
||||
return Response({"ok": False, "code": exc.code, "error": exc.message}, status=exc.response_status)
|
||||
|
||||
if preflight_error:
|
||||
error_payload = {
|
||||
"ok": False,
|
||||
"voice_session_id": str(recorded_session.id),
|
||||
"code": preflight_error["code"],
|
||||
"error": preflight_error["message"],
|
||||
"limit_scope": preflight_error.get("scope"),
|
||||
"limit_window": preflight_error.get("window"),
|
||||
"limit": preflight_error.get("limit"),
|
||||
"used": preflight_error.get("used"),
|
||||
"window_seconds": preflight_error.get("window_seconds"),
|
||||
"retry_after": preflight_error.get("retry_after"),
|
||||
"project_id": preflight_error.get("project_id"),
|
||||
"project_identifier": preflight_error.get("project_identifier"),
|
||||
"project_name": preflight_error.get("project_name"),
|
||||
}
|
||||
if preflight_error.get("reset_at"):
|
||||
error_payload["reset_at"] = preflight_error["reset_at"].isoformat()
|
||||
return Response(error_payload, status=status.HTTP_429_TOO_MANY_REQUESTS)
|
||||
|
||||
from plane.bgtasks.voice_tasker_task import process_voice_task_session
|
||||
|
||||
try:
|
||||
process_voice_task_session.delay(str(voice_session.id))
|
||||
except Exception as exc:
|
||||
log_exception(exc)
|
||||
voice_session.status = VoiceTaskSession.Status.FAILED
|
||||
voice_session.error_code = "voice_task_queue_unavailable"
|
||||
voice_session.error_message = "Voice Tasker queue is not available."
|
||||
voice_session.save(update_fields=["status", "error_code", "error_message", "updated_at"])
|
||||
clear_voice_task_audio_file(voice_session)
|
||||
return Response(
|
||||
{
|
||||
"ok": False,
|
||||
"voice_session_id": str(voice_session.id),
|
||||
"code": rate_limit_error["code"],
|
||||
"error": rate_limit_error["message"],
|
||||
"limit_scope": rate_limit_error["scope"],
|
||||
"limit_window": rate_limit_error["window"],
|
||||
"limit": rate_limit_error["limit"],
|
||||
"used": rate_limit_error["used"],
|
||||
"window_seconds": rate_limit_error["window_seconds"],
|
||||
"retry_after": rate_limit_error["retry_after"],
|
||||
"reset_at": rate_limit_error["reset_at"].isoformat(),
|
||||
"project_id": rate_limit_error["project_id"],
|
||||
"project_identifier": rate_limit_error["project_identifier"],
|
||||
"project_name": rate_limit_error["project_name"],
|
||||
"code": voice_session.error_code,
|
||||
"error": voice_session.error_message,
|
||||
},
|
||||
status=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
)
|
||||
|
||||
voice_session = VoiceTaskSession.objects.create(
|
||||
workspace=workspace,
|
||||
user=request.user,
|
||||
project=quota_project,
|
||||
status=VoiceTaskSession.Status.UPLOADED,
|
||||
audio_duration_seconds=duration_seconds,
|
||||
audio_content_type=audio_content_type,
|
||||
audio_size=audio.size,
|
||||
client_context=client_context,
|
||||
return Response(serialize_voice_task_session_response(voice_session), status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
|
||||
class VoiceTaskSessionEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug, session_id):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
voice_session = (
|
||||
VoiceTaskSession.objects.select_related("workspace", "user", "project")
|
||||
.filter(workspace=workspace, user=request.user, id=session_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
try:
|
||||
ai_settings, api_key = get_workspace_ai_runtime(workspace)
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.TRANSCRIBING
|
||||
voice_session.save(update_fields=["status", "updated_at"])
|
||||
|
||||
transcript = OpenAITranscriptionService(
|
||||
api_key=api_key,
|
||||
model=ai_settings.transcription_model,
|
||||
).transcribe(audio, language=get_client_language(client_context))
|
||||
|
||||
if not transcript:
|
||||
raise VoiceTaskerPipelineError(
|
||||
"empty_transcript",
|
||||
"OpenAI returned an empty transcript.",
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.TRANSCRIBED
|
||||
voice_session.transcript = transcript
|
||||
voice_session.save(update_fields=["status", "transcript", "updated_at"])
|
||||
|
||||
parser_context = build_voice_task_parser_context(
|
||||
workspace=workspace,
|
||||
user=request.user,
|
||||
transcript=transcript,
|
||||
client_context=client_context,
|
||||
)
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.PARSING
|
||||
voice_session.save(update_fields=["status", "updated_at"])
|
||||
|
||||
parsed = VoiceTaskParserService(
|
||||
api_key=api_key,
|
||||
model=ai_settings.structuring_model,
|
||||
).parse(parser_context)
|
||||
if not parsed.get("state_hint"):
|
||||
inferred_state_hint = infer_voice_task_state_hint(transcript)
|
||||
if inferred_state_hint:
|
||||
parsed["state_hint"] = inferred_state_hint
|
||||
parsed = harden_voice_task_intent(parsed, transcript)
|
||||
|
||||
warnings = get_voice_task_warnings(parsed, transcript)
|
||||
resolution = build_voice_task_resolution(
|
||||
workspace=workspace,
|
||||
user=request.user,
|
||||
ai_settings=ai_settings,
|
||||
draft=parsed,
|
||||
client_context=client_context,
|
||||
transcript=transcript,
|
||||
)
|
||||
warnings = list(dict.fromkeys(warnings + resolution["warnings"]))
|
||||
requires_confirmation = voice_task_requires_confirmation(parsed, warnings)
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.PARSED
|
||||
voice_session.intent = parsed["intent"]
|
||||
voice_session.parsed_json = parsed
|
||||
if not voice_session.project_id and parsed.get("project_id"):
|
||||
voice_session.project = get_voice_task_quota_project(workspace, project_id=parsed.get("project_id"))
|
||||
voice_session.save(update_fields=["status", "intent", "parsed_json", "project", "updated_at"])
|
||||
|
||||
if not voice_session:
|
||||
return Response(
|
||||
{
|
||||
"ok": True,
|
||||
"status": "parsed",
|
||||
"pipeline_status": "parsed",
|
||||
"voice_session_id": str(voice_session.id),
|
||||
"transcript": transcript,
|
||||
"intent": parsed["intent"],
|
||||
"draft": parsed,
|
||||
"resolution": resolution,
|
||||
"warnings": warnings,
|
||||
"requires_confirmation": requires_confirmation,
|
||||
"models": {
|
||||
"transcription": ai_settings.transcription_model,
|
||||
"structuring": ai_settings.structuring_model,
|
||||
},
|
||||
"audio": {
|
||||
"content_type": audio_content_type,
|
||||
"duration_seconds": duration_seconds,
|
||||
"size": audio.size,
|
||||
},
|
||||
"client_context": client_context,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
{"ok": False, "code": "voice_session_not_found", "error": "Voice Task session was not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
except VoiceTaskerPipelineError as exc:
|
||||
pipeline_error = exc
|
||||
except Exception as exc:
|
||||
pipeline_error = get_openai_pipeline_error(exc)
|
||||
|
||||
voice_session.status = VoiceTaskSession.Status.FAILED
|
||||
voice_session.error_code = pipeline_error.code
|
||||
voice_session.error_message = pipeline_error.message
|
||||
voice_session.save(update_fields=["status", "error_code", "error_message", "updated_at"])
|
||||
|
||||
return Response(
|
||||
{
|
||||
"ok": False,
|
||||
"voice_session_id": str(voice_session.id),
|
||||
"code": pipeline_error.code,
|
||||
"error": pipeline_error.message,
|
||||
},
|
||||
status=pipeline_error.response_status,
|
||||
)
|
||||
response_status = status.HTTP_400_BAD_REQUEST if voice_session.status == VoiceTaskSession.Status.FAILED else status.HTTP_200_OK
|
||||
return Response(serialize_voice_task_session_response(voice_session), status=response_status)
|
||||
|
||||
|
||||
class VoiceTaskCommitEndpoint(BaseAPIView):
|
||||
|
||||
Reference in New Issue
Block a user