p0aiapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- p0aiapi/__init__.py +46 -0
- p0aiapi/__main__.py +3 -0
- p0aiapi/audio.py +117 -0
- p0aiapi/catalog.py +21 -0
- p0aiapi/cli.py +78 -0
- p0aiapi/client.py +498 -0
- p0aiapi/exceptions.py +60 -0
- p0aiapi/models.py +11 -0
- p0aiapi/openapi.py +406 -0
- p0aiapi/py.typed +0 -0
- p0aiapi/resources.py +245 -0
- p0aiapi/schemas/yapi-8.openapi.json +1826 -0
- p0aiapi/streaming.py +194 -0
- p0aiapi-0.1.0.dist-info/METADATA +227 -0
- p0aiapi-0.1.0.dist-info/RECORD +17 -0
- p0aiapi-0.1.0.dist-info/WHEEL +4 -0
- p0aiapi-0.1.0.dist-info/entry_points.txt +2 -0
p0aiapi/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Public interface for p0aiapi."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from .catalog import load_catalog
|
|
6
|
+
from .client import AsyncClient, Client, P0ResponseError
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
APIError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
P0Error,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
ServerError,
|
|
14
|
+
TaskFailedError,
|
|
15
|
+
TaskTimeoutError,
|
|
16
|
+
TransportError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
from .models import TaskStatus
|
|
20
|
+
from .openapi import OpenAPISchema, Operation, SchemaError, diff_schemas
|
|
21
|
+
from .streaming import ServerSentEvent
|
|
22
|
+
|
|
23
|
+
__version__ = version("p0aiapi")
|
|
24
|
+
__all__ = [
|
|
25
|
+
"APIError",
|
|
26
|
+
"AsyncClient",
|
|
27
|
+
"AuthenticationError",
|
|
28
|
+
"Client",
|
|
29
|
+
"P0Error",
|
|
30
|
+
"P0ResponseError",
|
|
31
|
+
"NotFoundError",
|
|
32
|
+
"OpenAPISchema",
|
|
33
|
+
"Operation",
|
|
34
|
+
"RateLimitError",
|
|
35
|
+
"SchemaError",
|
|
36
|
+
"ServerError",
|
|
37
|
+
"ServerSentEvent",
|
|
38
|
+
"TaskFailedError",
|
|
39
|
+
"TaskStatus",
|
|
40
|
+
"TaskTimeoutError",
|
|
41
|
+
"TransportError",
|
|
42
|
+
"ValidationError",
|
|
43
|
+
"__version__",
|
|
44
|
+
"diff_schemas",
|
|
45
|
+
"load_catalog",
|
|
46
|
+
]
|
p0aiapi/__main__.py
ADDED
p0aiapi/audio.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Conveniences for the backend's public audio and TTS v2 APIs.
|
|
2
|
+
|
|
3
|
+
Generation returns the initial task record. List and upload responses remain in
|
|
4
|
+
the server's original shape, including fields introduced by later API versions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import mimetypes
|
|
10
|
+
from os import PathLike
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .resources import _AsyncClient, _Client, _provided
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _content_type(source: Path) -> str:
|
|
18
|
+
if source.suffix.lower() == ".wav":
|
|
19
|
+
return "audio/wav"
|
|
20
|
+
return mimetypes.guess_type(source.name)[0] or "application/octet-stream"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Audio:
|
|
24
|
+
"""TTS v1 voice management and text-to-speech task submission."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, client: _Client) -> None:
|
|
27
|
+
self._client = client
|
|
28
|
+
|
|
29
|
+
def list_voices(self) -> Any:
|
|
30
|
+
"""Return the available voice IDs in the backend's response shape."""
|
|
31
|
+
return self._client.request("GET", "/api/public/generate/audio/list")
|
|
32
|
+
|
|
33
|
+
def generate(self, text: str, **fields: Any) -> Any:
|
|
34
|
+
"""Submit audio generation; fields may include spk_id and task_id.
|
|
35
|
+
|
|
36
|
+
Optional values are omitted when None, so the backend controls defaults.
|
|
37
|
+
The SDK sends text unchanged; any further processing belongs to the API.
|
|
38
|
+
"""
|
|
39
|
+
return self._client.request(
|
|
40
|
+
"POST", "/api/public/generate/audio", data=_provided({"text": text, **fields})
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def upload_voice(self, spk_id: str, prompt_text: str, path: str | PathLike[str]) -> Any:
|
|
44
|
+
"""Upload a local voice sample with its transcript.
|
|
45
|
+
|
|
46
|
+
The backend currently requires a WAV sample of 5–30 seconds. The local
|
|
47
|
+
file is closed whether the upload succeeds or raises an exception.
|
|
48
|
+
"""
|
|
49
|
+
source = Path(path)
|
|
50
|
+
with source.open("rb") as stream:
|
|
51
|
+
return self._client.request(
|
|
52
|
+
"POST",
|
|
53
|
+
"/api/public/generate/audio/upload",
|
|
54
|
+
data={"spk_id": spk_id, "prompt_text": prompt_text},
|
|
55
|
+
files={"prompt_audio": (source.name, stream, _content_type(source))},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AsyncAudio:
|
|
60
|
+
"""Asynchronous counterpart of :class:`Audio`."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, client: _AsyncClient) -> None:
|
|
63
|
+
self._client = client
|
|
64
|
+
|
|
65
|
+
async def list_voices(self) -> Any:
|
|
66
|
+
return await self._client.request("GET", "/api/public/generate/audio/list")
|
|
67
|
+
|
|
68
|
+
async def generate(self, text: str, **fields: Any) -> Any:
|
|
69
|
+
return await self._client.request(
|
|
70
|
+
"POST", "/api/public/generate/audio", data=_provided({"text": text, **fields})
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def upload_voice(self, spk_id: str, prompt_text: str, path: str | PathLike[str]) -> Any:
|
|
74
|
+
"""Upload a WAV sample and transcript; close the file after completion."""
|
|
75
|
+
source = Path(path)
|
|
76
|
+
with source.open("rb") as stream:
|
|
77
|
+
return await self._client.request(
|
|
78
|
+
"POST",
|
|
79
|
+
"/api/public/generate/audio/upload",
|
|
80
|
+
data={"spk_id": spk_id, "prompt_text": prompt_text},
|
|
81
|
+
files={"prompt_audio": (source.name, stream, _content_type(source))},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TTS:
|
|
86
|
+
"""TTS v2 speaker management and text-to-speech task submission."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, client: _Client) -> None:
|
|
89
|
+
self._client = client
|
|
90
|
+
|
|
91
|
+
def list_speakers(self) -> Any:
|
|
92
|
+
return self._client.request("GET", "/api/public/generate/ttsv2/speakers")
|
|
93
|
+
|
|
94
|
+
def generate(self, text: str, spk_id: str, **fields: Any) -> Any:
|
|
95
|
+
"""Submit TTS v2; optional fields include use_emo, emo_type, and task_id."""
|
|
96
|
+
return self._client.request(
|
|
97
|
+
"POST",
|
|
98
|
+
"/api/public/generate/ttsv2",
|
|
99
|
+
data=_provided({"text": text, "spk_id": spk_id, **fields}),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class AsyncTTS:
|
|
104
|
+
"""Asynchronous counterpart of :class:`TTS`."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, client: _AsyncClient) -> None:
|
|
107
|
+
self._client = client
|
|
108
|
+
|
|
109
|
+
async def list_speakers(self) -> Any:
|
|
110
|
+
return await self._client.request("GET", "/api/public/generate/ttsv2/speakers")
|
|
111
|
+
|
|
112
|
+
async def generate(self, text: str, spk_id: str, **fields: Any) -> Any:
|
|
113
|
+
return await self._client.request(
|
|
114
|
+
"POST",
|
|
115
|
+
"/api/public/generate/ttsv2",
|
|
116
|
+
data=_provided({"text": text, "spk_id": spk_id, **fields}),
|
|
117
|
+
)
|
p0aiapi/catalog.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Load the packaged, reviewed catalogue without a network request.
|
|
2
|
+
|
|
3
|
+
The catalogue covers a supported subset of YApi project 8 matched to registered
|
|
4
|
+
backend routes. It is neither the full backend API nor a live deployment schema.
|
|
5
|
+
Use an explicitly fetched deployment schema when current server metadata is
|
|
6
|
+
needed. Catalogue IDs such as ``yapi_2527`` remain independent of FastAPI's
|
|
7
|
+
generated operation IDs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from importlib.resources import files
|
|
14
|
+
|
|
15
|
+
from .openapi import OpenAPISchema
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_catalog() -> OpenAPISchema:
|
|
19
|
+
"""Return a fresh offline schema containing the 27 reviewed YApi operations."""
|
|
20
|
+
resource = files("p0aiapi").joinpath("schemas").joinpath("yapi-8.openapi.json")
|
|
21
|
+
return OpenAPISchema(json.loads(resource.read_text(encoding="utf-8")))
|
p0aiapi/cli.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Download and inspect deployment-specific OpenAPI snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .catalog import load_catalog
|
|
13
|
+
from .client import DEFAULT_BASE_URL, Client
|
|
14
|
+
from .exceptions import P0Error
|
|
15
|
+
from .openapi import OpenAPISchema, diff_schemas
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _write_snapshot(path: Path, document: dict) -> None:
|
|
19
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
filename = None
|
|
21
|
+
try:
|
|
22
|
+
with tempfile.NamedTemporaryFile(
|
|
23
|
+
mode="w", encoding="utf-8", dir=path.parent, delete=False
|
|
24
|
+
) as file:
|
|
25
|
+
filename = file.name
|
|
26
|
+
json.dump(document, file, ensure_ascii=False, indent=2)
|
|
27
|
+
file.write("\n")
|
|
28
|
+
os.replace(filename, path)
|
|
29
|
+
finally:
|
|
30
|
+
if filename is not None and os.path.exists(filename):
|
|
31
|
+
os.unlink(filename)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main(argv: list[str] | None = None) -> int:
|
|
35
|
+
parser = argparse.ArgumentParser(prog="p0aiapi", description="P0 OpenAPI snapshot tools")
|
|
36
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
37
|
+
fetch = sub.add_parser("fetch-schema", help="Download and validate /openapi.json")
|
|
38
|
+
fetch.add_argument(
|
|
39
|
+
"--base-url", help=f"Service root; defaults to P0_API_BASE_URL or {DEFAULT_BASE_URL}"
|
|
40
|
+
)
|
|
41
|
+
fetch.add_argument("--path", default="/openapi.json", help="Schema path inside this deployment")
|
|
42
|
+
fetch.add_argument("--output", required=True, type=Path)
|
|
43
|
+
listing = sub.add_parser("operations", help="List operation IDs, HTTP methods and paths")
|
|
44
|
+
listing.add_argument("schema", type=Path, nargs="?", help="Defaults to the bundled catalog")
|
|
45
|
+
export = sub.add_parser("export-catalog", help="Save the bundled, verified YApi subset")
|
|
46
|
+
export.add_argument("--output", required=True, type=Path)
|
|
47
|
+
diff = sub.add_parser("diff", help="Compare two schema snapshots (not a compatibility proof)")
|
|
48
|
+
diff.add_argument("old", type=Path)
|
|
49
|
+
diff.add_argument("new", type=Path)
|
|
50
|
+
diff.add_argument("--fail-on-change", action="store_true", help="Exit 2 on any difference")
|
|
51
|
+
args = parser.parse_args(argv)
|
|
52
|
+
try:
|
|
53
|
+
if args.command == "fetch-schema":
|
|
54
|
+
with Client(args.base_url) as client:
|
|
55
|
+
schema = client.refresh_openapi(args.path)
|
|
56
|
+
_write_snapshot(args.output, schema.document)
|
|
57
|
+
print(f"Saved {len(schema.operations)} operations to {args.output}")
|
|
58
|
+
elif args.command == "operations":
|
|
59
|
+
schema = OpenAPISchema.load(args.schema) if args.schema else load_catalog()
|
|
60
|
+
for operation in schema.operations.values():
|
|
61
|
+
print(f"{operation.id}\t{operation.method}\t{operation.path}")
|
|
62
|
+
elif args.command == "export-catalog":
|
|
63
|
+
schema = load_catalog()
|
|
64
|
+
_write_snapshot(args.output, schema.document)
|
|
65
|
+
print(f"Saved {len(schema.operations)} bundled operations to {args.output}")
|
|
66
|
+
else:
|
|
67
|
+
result = diff_schemas(OpenAPISchema.load(args.old), OpenAPISchema.load(args.new))
|
|
68
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
69
|
+
if args.fail_on_change and any(result.values()):
|
|
70
|
+
return 2
|
|
71
|
+
except (P0Error, ValueError, OSError) as exc:
|
|
72
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
73
|
+
return 1
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
raise SystemExit(main())
|