mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 22:15:10 +00:00
The backend import package is now docsgpt, the name it will carry on PyPI; application was far too generic to install into anyone's site-packages. git mv plus a mechanical rewrite of every import, dotted string and path reference: 734 Python files, the compose files, Dockerfile, workflows, docs, setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage config, .gitignore. Behaviour is unchanged. Kept for one release: - A top-level application package whose meta-path finder resolves application.x.y to the already-imported docsgpt.x.y object, so old imports and entry points (celery -A application.app.celery, uvicorn application.asgi:asgi_app) keep working with a FutureWarning. - Celery registers every application.* task name as an alias of its docsgpt.* task on start-up, so messages queued by the previous release still run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries the previous release wrote are left unread instead of firing twice. The backend image builds from the repository root (docker build -f docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore allow-lists docsgpt/ and application/ and keeps caches, local data, .env files, the sample index files and the Dockerfile out. Compose and the image workflows point at the new context.
276 lines
9.1 KiB
Python
276 lines
9.1 KiB
Python
"""Tests for docsgpt/stt/openai_stt.py"""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch, mock_open
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOpenAISTTInit:
|
|
|
|
@patch("docsgpt.stt.openai_stt.OpenAI")
|
|
@patch("docsgpt.stt.openai_stt.settings")
|
|
def test_init_defaults_from_settings(self, mock_settings, mock_openai_cls):
|
|
mock_settings.OPENAI_API_KEY = "sk-from-settings"
|
|
mock_settings.API_KEY = "sk-fallback"
|
|
mock_settings.OPENAI_BASE_URL = "https://custom.api.com/v1"
|
|
mock_settings.OPENAI_STT_MODEL = "whisper-1"
|
|
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
stt = OpenAISTT()
|
|
|
|
assert stt.api_key == "sk-from-settings"
|
|
assert stt.base_url == "https://custom.api.com/v1"
|
|
assert stt.model == "whisper-1"
|
|
mock_openai_cls.assert_called_once_with(
|
|
api_key="sk-from-settings",
|
|
base_url="https://custom.api.com/v1",
|
|
)
|
|
|
|
@patch("docsgpt.stt.openai_stt.OpenAI")
|
|
@patch("docsgpt.stt.openai_stt.settings")
|
|
def test_init_explicit_params_override_settings(self, mock_settings, mock_openai_cls):
|
|
mock_settings.OPENAI_API_KEY = "sk-settings"
|
|
mock_settings.API_KEY = None
|
|
mock_settings.OPENAI_BASE_URL = None
|
|
mock_settings.OPENAI_STT_MODEL = "whisper-1"
|
|
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
stt = OpenAISTT(
|
|
api_key="sk-explicit",
|
|
base_url="https://explicit.api.com",
|
|
model="whisper-2",
|
|
)
|
|
|
|
assert stt.api_key == "sk-explicit"
|
|
assert stt.base_url == "https://explicit.api.com"
|
|
assert stt.model == "whisper-2"
|
|
|
|
@patch("docsgpt.stt.openai_stt.OpenAI")
|
|
@patch("docsgpt.stt.openai_stt.settings")
|
|
def test_init_falls_back_to_api_key(self, mock_settings, mock_openai_cls):
|
|
mock_settings.OPENAI_API_KEY = None
|
|
mock_settings.API_KEY = "sk-fallback-key"
|
|
mock_settings.OPENAI_BASE_URL = None
|
|
mock_settings.OPENAI_STT_MODEL = "whisper-1"
|
|
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
stt = OpenAISTT()
|
|
|
|
assert stt.api_key == "sk-fallback-key"
|
|
|
|
@patch("docsgpt.stt.openai_stt.OpenAI")
|
|
@patch("docsgpt.stt.openai_stt.settings")
|
|
def test_init_default_base_url(self, mock_settings, mock_openai_cls):
|
|
mock_settings.OPENAI_API_KEY = "sk-test"
|
|
mock_settings.API_KEY = None
|
|
mock_settings.OPENAI_BASE_URL = None
|
|
mock_settings.OPENAI_STT_MODEL = "whisper-1"
|
|
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
stt = OpenAISTT()
|
|
|
|
assert stt.base_url == "https://api.openai.com/v1"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOpenAISTTTranscribe:
|
|
|
|
@patch("docsgpt.stt.openai_stt.OpenAI")
|
|
@patch("docsgpt.stt.openai_stt.settings")
|
|
def test_transcribe_basic(self, mock_settings, mock_openai_cls):
|
|
mock_settings.OPENAI_API_KEY = "sk-test"
|
|
mock_settings.API_KEY = None
|
|
mock_settings.OPENAI_BASE_URL = None
|
|
mock_settings.OPENAI_STT_MODEL = "whisper-1"
|
|
|
|
mock_client = MagicMock()
|
|
mock_openai_cls.return_value = mock_client
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.model_dump.return_value = {
|
|
"text": "Hello world",
|
|
"language": "en",
|
|
"duration": 2.5,
|
|
"segments": [],
|
|
}
|
|
mock_client.audio.transcriptions.create.return_value = mock_response
|
|
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
stt = OpenAISTT()
|
|
|
|
file_path = Path("/tmp/test_audio.wav")
|
|
with patch("builtins.open", mock_open(read_data=b"audio_data")):
|
|
result = stt.transcribe(file_path)
|
|
|
|
assert result["text"] == "Hello world"
|
|
assert result["language"] == "en"
|
|
assert result["duration_s"] == 2.5
|
|
assert result["segments"] == []
|
|
assert result["provider"] == "openai"
|
|
|
|
@patch("docsgpt.stt.openai_stt.OpenAI")
|
|
@patch("docsgpt.stt.openai_stt.settings")
|
|
def test_transcribe_with_language(self, mock_settings, mock_openai_cls):
|
|
mock_settings.OPENAI_API_KEY = "sk-test"
|
|
mock_settings.API_KEY = None
|
|
mock_settings.OPENAI_BASE_URL = None
|
|
mock_settings.OPENAI_STT_MODEL = "whisper-1"
|
|
|
|
mock_client = MagicMock()
|
|
mock_openai_cls.return_value = mock_client
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.model_dump.return_value = {
|
|
"text": "Bonjour",
|
|
"language": "fr",
|
|
"duration": 1.0,
|
|
"segments": [],
|
|
}
|
|
mock_client.audio.transcriptions.create.return_value = mock_response
|
|
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
stt = OpenAISTT()
|
|
|
|
file_path = Path("/tmp/test_audio.wav")
|
|
with patch("builtins.open", mock_open(read_data=b"audio_data")):
|
|
result = stt.transcribe(file_path, language="fr")
|
|
|
|
assert result["language"] == "fr"
|
|
call_kwargs = mock_client.audio.transcriptions.create.call_args[1]
|
|
assert call_kwargs["language"] == "fr"
|
|
|
|
@patch("docsgpt.stt.openai_stt.OpenAI")
|
|
@patch("docsgpt.stt.openai_stt.settings")
|
|
def test_transcribe_with_timestamps(self, mock_settings, mock_openai_cls):
|
|
mock_settings.OPENAI_API_KEY = "sk-test"
|
|
mock_settings.API_KEY = None
|
|
mock_settings.OPENAI_BASE_URL = None
|
|
mock_settings.OPENAI_STT_MODEL = "whisper-1"
|
|
|
|
mock_client = MagicMock()
|
|
mock_openai_cls.return_value = mock_client
|
|
|
|
segment_obj = MagicMock()
|
|
segment_obj.model_dump.return_value = {
|
|
"start": 0.0,
|
|
"end": 1.5,
|
|
"text": "Hello",
|
|
}
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.model_dump.return_value = {
|
|
"text": "Hello",
|
|
"language": "en",
|
|
"duration": 1.5,
|
|
"segments": [segment_obj],
|
|
}
|
|
mock_client.audio.transcriptions.create.return_value = mock_response
|
|
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
stt = OpenAISTT()
|
|
|
|
file_path = Path("/tmp/test_audio.wav")
|
|
with patch("builtins.open", mock_open(read_data=b"audio_data")):
|
|
result = stt.transcribe(file_path, timestamps=True)
|
|
|
|
call_kwargs = mock_client.audio.transcriptions.create.call_args[1]
|
|
assert call_kwargs["timestamp_granularities"] == ["segment"]
|
|
assert len(result["segments"]) == 1
|
|
|
|
@patch("docsgpt.stt.openai_stt.OpenAI")
|
|
@patch("docsgpt.stt.openai_stt.settings")
|
|
def test_transcribe_no_segments_key(self, mock_settings, mock_openai_cls):
|
|
mock_settings.OPENAI_API_KEY = "sk-test"
|
|
mock_settings.API_KEY = None
|
|
mock_settings.OPENAI_BASE_URL = None
|
|
mock_settings.OPENAI_STT_MODEL = "whisper-1"
|
|
|
|
mock_client = MagicMock()
|
|
mock_openai_cls.return_value = mock_client
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.model_dump.return_value = {
|
|
"text": "Hello",
|
|
}
|
|
mock_client.audio.transcriptions.create.return_value = mock_response
|
|
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
stt = OpenAISTT()
|
|
|
|
file_path = Path("/tmp/test_audio.wav")
|
|
with patch("builtins.open", mock_open(read_data=b"audio_data")):
|
|
result = stt.transcribe(file_path)
|
|
|
|
assert result["text"] == "Hello"
|
|
assert result["segments"] == []
|
|
|
|
@patch("docsgpt.stt.openai_stt.OpenAI")
|
|
@patch("docsgpt.stt.openai_stt.settings")
|
|
def test_transcribe_language_fallback_to_param(self, mock_settings, mock_openai_cls):
|
|
mock_settings.OPENAI_API_KEY = "sk-test"
|
|
mock_settings.API_KEY = None
|
|
mock_settings.OPENAI_BASE_URL = None
|
|
mock_settings.OPENAI_STT_MODEL = "whisper-1"
|
|
|
|
mock_client = MagicMock()
|
|
mock_openai_cls.return_value = mock_client
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.model_dump.return_value = {
|
|
"text": "Test",
|
|
"language": None,
|
|
"duration": 1.0,
|
|
}
|
|
mock_client.audio.transcriptions.create.return_value = mock_response
|
|
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
stt = OpenAISTT()
|
|
|
|
file_path = Path("/tmp/test_audio.wav")
|
|
with patch("builtins.open", mock_open(read_data=b"audio_data")):
|
|
result = stt.transcribe(file_path, language="de")
|
|
|
|
assert result["language"] == "de"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestOpenAISTTToDict:
|
|
|
|
def test_to_dict_with_model_dump(self):
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
obj = MagicMock()
|
|
obj.model_dump.return_value = {"key": "value"}
|
|
|
|
result = OpenAISTT._to_dict(obj)
|
|
assert result == {"key": "value"}
|
|
|
|
def test_to_dict_with_dict(self):
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
result = OpenAISTT._to_dict({"key": "value"})
|
|
assert result == {"key": "value"}
|
|
|
|
def test_to_dict_with_other_type(self):
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
result = OpenAISTT._to_dict("string_value")
|
|
assert result == {}
|
|
|
|
def test_to_dict_with_none(self):
|
|
from docsgpt.stt.openai_stt import OpenAISTT
|
|
|
|
result = OpenAISTT._to_dict(None)
|
|
assert result == {}
|