codai-sdk 0.67.0__tar.gz → 0.69.0__tar.gz
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.
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/CHANGELOG.md +24 -3
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/PKG-INFO +1 -1
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/pyproject.toml +1 -1
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/__init__.py +7 -1
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/_operations.py +8 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/_types.py +334 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/__init__.py +6 -0
- codai_sdk-0.69.0/src/codai/resources/music_room.py +164 -0
- codai_sdk-0.69.0/src/codai/resources/voices.py +39 -0
- codai_sdk-0.69.0/tests/test_music_room.py +94 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_resources.py +1 -1
- codai_sdk-0.69.0/tests/test_voices.py +28 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/dependabot.yml +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/pull_request_template.md +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/workflows/publish.yml +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.gitignore +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/CODE_OF_CONDUCT.md +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/CONTRIBUTING.md +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/LICENSE +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/README.md +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/SECURITY.md +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/examples/agent.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/examples/chat.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/examples/feedback.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/examples/streaming.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/scripts/gen-types.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/scripts/smoke_local.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/_http.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/_resolve_types.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/_base.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/abuse_reports.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/agents.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/batches.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/billing.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/brand_kits.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/cards.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/chat.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/core.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/creatives.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/environments.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/exports.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/lists.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/lyrics.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/media_ops.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/memory.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/messages.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/platform.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/productions.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/prompts.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/provenance.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/referrals.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/research.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/resolve.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/responses.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/sandboxes.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/schedules.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/sessions.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/shares.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/templates.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/transcripts.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/triggers.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/usage_keys.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/vault.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/voice_revocations.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/conftest.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_abuse_reports.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_batches.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_billing_referrals.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_brand_kits.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_cards.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives_cx07.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives_documents.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives_e11.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives_e11b.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_environments_exec.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_exports.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_http.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_lists.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_lyrics.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_media_ops.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_memory.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_os_gate.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_parity.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_productions.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_prompts.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_provenance.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_research.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_resolve.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_sandboxes.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_settings.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_shares_e12a.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_templates.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_transcripts.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_vault.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_voice_revocations.py +0 -0
- {codai_sdk-0.67.0 → codai_sdk-0.69.0}/uv.lock +0 -0
|
@@ -6,6 +6,27 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.69.0] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
- `MusicRoom.listening`: what the Music Room critic heard (listening features, Audiobox Aesthetics, Gemini listener
|
|
12
|
+
verdict) — new `MusicRoomListening` TypedDict; `None` before the critic ran. The `critic_text_only` warning is now
|
|
13
|
+
documented (the critic could not listen to the master and judged from the score).
|
|
14
|
+
|
|
15
|
+
## [0.68.0] - 2026-10-22
|
|
16
|
+
|
|
17
|
+
- Music Room (MR-09..MR-17): new `client.studio` namespace with `studio.music_room` — a team of role agents writes,
|
|
18
|
+
plays, sings and mixes a song with three approval gates. `create(brief, language=, genre=, duration_seconds=,
|
|
19
|
+
vocals=, timbre=, voice_passport_id=, instruments=, auto=, budget_micro_usd=, title=, idempotency_key=)`
|
|
20
|
+
(`POST /v1/studio/music-room`), `get(id)`, `list(limit=, before=)`, `quota()`, `choose(id, gate, variant=,
|
|
21
|
+
approve=)`, `direct(id, role, direction, section_id=, idempotency_key=)` and `wait(id, until_gate=, interval=,
|
|
22
|
+
timeout=)`; constant `MUSIC_ROOM_TERMINAL_STATUSES`.
|
|
23
|
+
- Regenerated `_types.py`: `MusicRoomCreate`, `MusicRoom`, `MusicRoomPlanVariant`, `MusicRole`, `MusicRoomChoose`,
|
|
24
|
+
`MusicRoomDirect`, `MusicRoomQuota`, `MusicRoomList`.
|
|
25
|
+
- Voice passport (D37, beta): new `client.voices` with `add_singing_consent(voice_id, session_id)`
|
|
26
|
+
(`POST /v1/studio/voices/{id}/singing-consent`, never retried) and `revoke_singing_consent(voice_id)` (204): your own
|
|
27
|
+
custom voice singing in Music Room songs, a separate revocable consent; the result approximates your timbre.
|
|
28
|
+
Regenerated `_types.py`: `StudioVoice`, `StudioVoiceSinging`, `StudioVoiceSingingConsentRequest`.
|
|
29
|
+
|
|
9
30
|
## [0.67.0] - 2026-10-20
|
|
10
31
|
|
|
11
32
|
- Creatives (CX-07/08/09): `creatives.build(..., format=, format_id=, crop_marks=)` builds carousel (`png` | `pdf`)
|
|
@@ -16,7 +37,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
16
37
|
- `creatives.transcript_cut(creative_id, node_id=, remove=, base_seq=, force=)`
|
|
17
38
|
(`POST /v1/studio/creatives/{id}/transcript-cut`): delete transcript word ranges from a video clip; free.
|
|
18
39
|
- `creatives.regenerate_node(creative_id, node_id, prompt=, model=, aspect_ratio=, base_seq=, force=,
|
|
19
|
-
|
|
40
|
+
idempotency_key=)` (`POST /v1/studio/creatives/{id}/nodes/{nodeId}/regenerate`): new picture for an image node,
|
|
20
41
|
cast-consistent; billed like an image generation.
|
|
21
42
|
- `media_ops.quote` / `run` / `run_and_wait` accept `mask_png_b64=` for `image.inpaint` / `image.erase`.
|
|
22
43
|
- `videos.create` / `images.generate` document `reference_asset_ids` (Studio image assets as references).
|
|
@@ -34,8 +55,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
34
55
|
- Creatives: `CreativeKind` gains `audio` (songs and podcasts); new generated types `CreativeAudio`,
|
|
35
56
|
`CreativeAudioTrack`, `CreativeAudioClip`, `CreativeAudioSection`, `CreativeAudioFx` and the media-op schemas.
|
|
36
57
|
- New `client.lyrics` (MU-06): `write(brief, language, genre=, mood=, structure=, syllables_per_line=,
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
rhyme_scheme=, lines_per_section=, title=, idempotency_key=)` (`POST /v1/studio/lyrics`) and `align(creative_id,
|
|
59
|
+
clip_id, lyrics_text, language=, apply=, base_seq=, retranscribe=, idempotency_key=)` (`POST /v1/studio/lyrics/align`,
|
|
39
60
|
timed lines + LRC).
|
|
40
61
|
- `client.creatives.render_audio(creative_id, formats=, stems=, lrc=, loudness=, title=, idempotency_key=)` (MU-05):
|
|
41
62
|
mixdown + mastering + exports of an `audio` creative. New generated types `StudioLyrics*`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codai-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.69.0
|
|
4
4
|
Summary: Official Python SDK for the codai AI gateway — chat, messages, responses, streaming, agents, sessions, hosts, tasks, account.
|
|
5
5
|
Project-URL: Homepage, https://codai.ro
|
|
6
6
|
Project-URL: Repository, https://github.com/codai-ro/codai-sdk-python
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codai-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.69.0"
|
|
8
8
|
description = "Official Python SDK for the codai AI gateway — chat, messages, responses, streaming, agents, sessions, hosts, tasks, account."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
|
|
@@ -52,6 +52,7 @@ from .resources import (
|
|
|
52
52
|
BrandKits,
|
|
53
53
|
Templates,
|
|
54
54
|
Productions,
|
|
55
|
+
Studio,
|
|
55
56
|
MediaOps,
|
|
56
57
|
Lyrics,
|
|
57
58
|
MEDIA_JOB_TERMINAL_STATUSES,
|
|
@@ -62,6 +63,7 @@ from .resources import (
|
|
|
62
63
|
Shares,
|
|
63
64
|
share_token_from,
|
|
64
65
|
VoiceRevocations,
|
|
66
|
+
Voices,
|
|
65
67
|
Vault,
|
|
66
68
|
vault_unlock_message,
|
|
67
69
|
AbuseReports,
|
|
@@ -166,7 +168,7 @@ __all__ = [
|
|
|
166
168
|
"share_token_from",
|
|
167
169
|
"vault_unlock_message",
|
|
168
170
|
]
|
|
169
|
-
__version__ = "0.
|
|
171
|
+
__version__ = "0.69.0"
|
|
170
172
|
class _CallableGroup:
|
|
171
173
|
"""A resource group that is also callable — keeps ``client.chat(...)``,
|
|
172
174
|
``client.embeddings(...)``, ``client.models()`` and ``client.feedback(...)``
|
|
@@ -244,6 +246,8 @@ class Codai:
|
|
|
244
246
|
self.templates = Templates(http)
|
|
245
247
|
# Studio productions: brief -> storyboard -> images + voice-over -> creative -> draft render -> review.
|
|
246
248
|
self.productions = Productions(http)
|
|
249
|
+
# codai Studio namespace: studio.music_room (a team of role agents makes a song with three approval gates).
|
|
250
|
+
self.studio = Studio(http)
|
|
247
251
|
# Studio media operations: background removal, upscale, inpaint, stems, denoise, MIDI, mastering, ACE-Step music.
|
|
248
252
|
self.media_ops = MediaOps(http)
|
|
249
253
|
# Studio lyrics: write (syllable + rhyme checked, token-metered) and align to an audio clip (LRC + setLyrics).
|
|
@@ -256,6 +260,8 @@ class Codai:
|
|
|
256
260
|
self.shares = Shares(http)
|
|
257
261
|
# Public takedown tickets for voices cloned in codai Studio (no key needed by the route).
|
|
258
262
|
self.voice_revocations = VoiceRevocations(http)
|
|
263
|
+
# Studio custom voices: the voice passport singing consent (beta; your own voice singing in Music Room songs).
|
|
264
|
+
self.voices = Voices(http)
|
|
259
265
|
# Zero-knowledge credentials vault: ciphertext only; the agent broker calls never return secret values.
|
|
260
266
|
self.vault = Vault(http)
|
|
261
267
|
# Public DSA notices about codai Studio share / review links (no key needed by the route).
|
|
@@ -104,6 +104,8 @@ OPERATION_METHODS: Dict[str, str] = {
|
|
|
104
104
|
"reopenPublicReviewThread": "shares.reopen",
|
|
105
105
|
"decidePublicReview": "shares.decide",
|
|
106
106
|
"createVoiceRevocation": "voice_revocations.create",
|
|
107
|
+
"addStudioVoiceSingingConsent": "voices.add_singing_consent",
|
|
108
|
+
"revokeStudioVoiceSingingConsent": "voices.revoke_singing_consent",
|
|
107
109
|
"getVaultAccount": "vault.account",
|
|
108
110
|
"putVaultAccount": "vault.put_account",
|
|
109
111
|
"patchVaultSettings": "vault.update_settings",
|
|
@@ -150,6 +152,12 @@ OPERATION_METHODS: Dict[str, str] = {
|
|
|
150
152
|
"listCreativeTemplates": "templates.list",
|
|
151
153
|
"createStudioProduction": "productions.create",
|
|
152
154
|
"getStudioProduction": "productions.retrieve",
|
|
155
|
+
"createStudioMusicRoom": "studio.music_room.create",
|
|
156
|
+
"listStudioMusicRooms": "studio.music_room.list",
|
|
157
|
+
"getStudioMusicRoomQuota": "studio.music_room.quota",
|
|
158
|
+
"getStudioMusicRoom": "studio.music_room.get",
|
|
159
|
+
"chooseStudioMusicRoom": "studio.music_room.choose",
|
|
160
|
+
"directStudioMusicRoom": "studio.music_room.direct",
|
|
153
161
|
"listStudioMediaOps": "media_ops.list",
|
|
154
162
|
"quoteStudioMediaOp": "media_ops.quote",
|
|
155
163
|
"runStudioMediaOp": "media_ops.run",
|
|
@@ -2562,6 +2562,233 @@ class StudioProduction(TypedDict):
|
|
|
2562
2562
|
created_at: str
|
|
2563
2563
|
|
|
2564
2564
|
|
|
2565
|
+
class _MusicRoomCreateRequired(TypedDict):
|
|
2566
|
+
brief: str
|
|
2567
|
+
|
|
2568
|
+
|
|
2569
|
+
class MusicRoomCreate(_MusicRoomCreateRequired, total=False):
|
|
2570
|
+
"""Music Room request"""
|
|
2571
|
+
language: Literal['ro', 'en']
|
|
2572
|
+
genre: str
|
|
2573
|
+
duration_seconds: int
|
|
2574
|
+
vocals: bool
|
|
2575
|
+
timbre: str
|
|
2576
|
+
voice_passport_id: str
|
|
2577
|
+
instruments: List[str]
|
|
2578
|
+
auto: bool
|
|
2579
|
+
budget_micro_usd: int
|
|
2580
|
+
title: str
|
|
2581
|
+
|
|
2582
|
+
|
|
2583
|
+
class MusicRoomPlanVariantSectionsItem(TypedDict):
|
|
2584
|
+
id: str
|
|
2585
|
+
kind: str
|
|
2586
|
+
bars: int
|
|
2587
|
+
|
|
2588
|
+
|
|
2589
|
+
# A member of the Music Room team.
|
|
2590
|
+
MusicRole = Literal['producer', 'composer', 'lyricist', 'arranger', 'drums', 'bass', 'guitar', 'keys', 'strings', 'brass', 'winds', 'balkan_lead', 'balkan_rhythm', 'percussion', 'synth', 'vocal_producer', 'sound_designer', 'mix_engineer', 'mastering_engineer', 'critic', 'rights']
|
|
2591
|
+
|
|
2592
|
+
|
|
2593
|
+
class MusicRoomPlanVariantInstrumentationItem(TypedDict):
|
|
2594
|
+
role: MusicRole
|
|
2595
|
+
instrument_id: str
|
|
2596
|
+
|
|
2597
|
+
|
|
2598
|
+
class MusicRoomPlanVariant(TypedDict):
|
|
2599
|
+
"""Music Room plan variant"""
|
|
2600
|
+
title: str
|
|
2601
|
+
genre_id: str
|
|
2602
|
+
bpm: float
|
|
2603
|
+
meter: str
|
|
2604
|
+
mode_id: str
|
|
2605
|
+
sections: List[MusicRoomPlanVariantSectionsItem]
|
|
2606
|
+
instrumentation: List[MusicRoomPlanVariantInstrumentationItem]
|
|
2607
|
+
rationale: str
|
|
2608
|
+
|
|
2609
|
+
|
|
2610
|
+
class _MusicRoomGateRequired(TypedDict):
|
|
2611
|
+
kind: Literal['plan', 'demo', 'final']
|
|
2612
|
+
since: str
|
|
2613
|
+
|
|
2614
|
+
|
|
2615
|
+
class MusicRoomGate(_MusicRoomGateRequired, total=False):
|
|
2616
|
+
"""The gate the song waits at (`null` when it is not waiting)."""
|
|
2617
|
+
variants: List[MusicRoomPlanVariant]
|
|
2618
|
+
|
|
2619
|
+
|
|
2620
|
+
class _MusicRoomTeamItemRequired(TypedDict):
|
|
2621
|
+
role: MusicRole
|
|
2622
|
+
name: str
|
|
2623
|
+
status: Literal['done', 'playing', 'waiting']
|
|
2624
|
+
|
|
2625
|
+
|
|
2626
|
+
class MusicRoomTeamItem(_MusicRoomTeamItemRequired, total=False):
|
|
2627
|
+
instrument_id: Optional[str]
|
|
2628
|
+
track_id: Optional[str]
|
|
2629
|
+
rationale: Optional[str]
|
|
2630
|
+
|
|
2631
|
+
|
|
2632
|
+
class _MusicRoomCritiqueIssuesItemRequired(TypedDict):
|
|
2633
|
+
role: MusicRole
|
|
2634
|
+
problem: str
|
|
2635
|
+
fix: str
|
|
2636
|
+
|
|
2637
|
+
|
|
2638
|
+
class MusicRoomCritiqueIssuesItem(_MusicRoomCritiqueIssuesItemRequired, total=False):
|
|
2639
|
+
section_id: Optional[str]
|
|
2640
|
+
|
|
2641
|
+
|
|
2642
|
+
class MusicRoomCritique(TypedDict):
|
|
2643
|
+
"""The critic's last review."""
|
|
2644
|
+
round: int
|
|
2645
|
+
scores: Dict[str, float]
|
|
2646
|
+
verdict: Literal['ship', 'revise']
|
|
2647
|
+
issues: List[MusicRoomCritiqueIssuesItem]
|
|
2648
|
+
|
|
2649
|
+
|
|
2650
|
+
class MusicRoomListeningFeaturesAesthetics(TypedDict):
|
|
2651
|
+
"""Meta Audiobox Aesthetics, about 1-10: CE content enjoyment, CU content usefulness, PC production complexity, P…"""
|
|
2652
|
+
CE: float
|
|
2653
|
+
CU: float
|
|
2654
|
+
PC: float
|
|
2655
|
+
PQ: float
|
|
2656
|
+
|
|
2657
|
+
|
|
2658
|
+
class MusicRoomListeningFeatures(TypedDict, total=False):
|
|
2659
|
+
"""Measured listening features of the master (codai-studio-media). `null` when the measurement was unavailable."""
|
|
2660
|
+
duration_seconds: Optional[float]
|
|
2661
|
+
spectral_centroid_hz: Optional[float]
|
|
2662
|
+
spectral_centroid_std_hz: Optional[float]
|
|
2663
|
+
spectral_flatness: Optional[float]
|
|
2664
|
+
onset_rate_per_s: Optional[float]
|
|
2665
|
+
onset_rate_cv: Optional[float]
|
|
2666
|
+
crest_factor_db: Optional[float]
|
|
2667
|
+
loudness_range_lu: Optional[float]
|
|
2668
|
+
stereo_width: Optional[float]
|
|
2669
|
+
loudness_timeline_lufs: List[Optional[float]]
|
|
2670
|
+
aesthetics: Optional[MusicRoomListeningFeaturesAesthetics]
|
|
2671
|
+
|
|
2672
|
+
|
|
2673
|
+
class MusicRoomListeningVerdictImpressions(TypedDict):
|
|
2674
|
+
"""Impressions from 0 to 10 per aspect; `vocals` is `null` for an instrumental."""
|
|
2675
|
+
groove: float
|
|
2676
|
+
melody: float
|
|
2677
|
+
mix: float
|
|
2678
|
+
vocals: Optional[float]
|
|
2679
|
+
humanness: float
|
|
2680
|
+
genre_fit: float
|
|
2681
|
+
|
|
2682
|
+
|
|
2683
|
+
class MusicRoomListeningVerdictNotesItem(TypedDict):
|
|
2684
|
+
at_seconds: float
|
|
2685
|
+
note: str
|
|
2686
|
+
|
|
2687
|
+
|
|
2688
|
+
class MusicRoomListeningVerdict(TypedDict):
|
|
2689
|
+
"""The Gemini listener's verdict. `null` when the listener was unavailable."""
|
|
2690
|
+
impressions: MusicRoomListeningVerdictImpressions
|
|
2691
|
+
notes: List[MusicRoomListeningVerdictNotesItem]
|
|
2692
|
+
ai_tells: List[str]
|
|
2693
|
+
summary: str
|
|
2694
|
+
|
|
2695
|
+
|
|
2696
|
+
class MusicRoomListening(TypedDict):
|
|
2697
|
+
"""Music Room listening"""
|
|
2698
|
+
features: Optional[MusicRoomListeningFeatures]
|
|
2699
|
+
verdict: Optional[MusicRoomListeningVerdict]
|
|
2700
|
+
model: Optional[str]
|
|
2701
|
+
|
|
2702
|
+
|
|
2703
|
+
class MusicRoomCopyRiskMatchesItem(TypedDict, total=False):
|
|
2704
|
+
id: str
|
|
2705
|
+
title: str
|
|
2706
|
+
score: float
|
|
2707
|
+
|
|
2708
|
+
|
|
2709
|
+
class MusicRoomCopyRisk(TypedDict):
|
|
2710
|
+
"""Melody similarity against the reference corpus."""
|
|
2711
|
+
max_score: float
|
|
2712
|
+
matches: List[MusicRoomCopyRiskMatchesItem]
|
|
2713
|
+
|
|
2714
|
+
|
|
2715
|
+
class MusicRoomQuota(TypedDict):
|
|
2716
|
+
within: bool
|
|
2717
|
+
used: int
|
|
2718
|
+
included: int
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
class MusicRoom(TypedDict):
|
|
2722
|
+
"""Music Room song"""
|
|
2723
|
+
id: str
|
|
2724
|
+
object: Literal['studio.music_room']
|
|
2725
|
+
status: Literal['running', 'waiting', 'succeeded', 'failed']
|
|
2726
|
+
phase: Literal['plan', 'gate_plan', 'compose', 'lyrics', 'arrange', 'parts', 'render_demo', 'gate_demo', 'vocals', 'mix', 'master', 'critique', 'gate_final', 'ready', 'failed']
|
|
2727
|
+
gate: Optional[MusicRoomGate]
|
|
2728
|
+
brief: str
|
|
2729
|
+
language: Literal['ro', 'en']
|
|
2730
|
+
genre_id: Optional[str]
|
|
2731
|
+
title: Optional[str]
|
|
2732
|
+
creative_id: Optional[str]
|
|
2733
|
+
demo_asset_id: Optional[str]
|
|
2734
|
+
final_asset_id: Optional[str]
|
|
2735
|
+
stems_asset_ids: List[str]
|
|
2736
|
+
team: List[MusicRoomTeamItem]
|
|
2737
|
+
critique: Optional[MusicRoomCritique]
|
|
2738
|
+
listening: Optional[MusicRoomListening]
|
|
2739
|
+
robot_score: Optional[float]
|
|
2740
|
+
copy_risk: Optional[MusicRoomCopyRisk]
|
|
2741
|
+
credits: List[str]
|
|
2742
|
+
quota: MusicRoomQuota
|
|
2743
|
+
budget_micro_usd: str
|
|
2744
|
+
spent_micro_usd: str
|
|
2745
|
+
warnings: List[str]
|
|
2746
|
+
error: Optional[str]
|
|
2747
|
+
created_at: str
|
|
2748
|
+
|
|
2749
|
+
|
|
2750
|
+
class MusicRoomChooseOption1(TypedDict):
|
|
2751
|
+
"""Plan gate answer"""
|
|
2752
|
+
gate: Literal['plan']
|
|
2753
|
+
variant: int
|
|
2754
|
+
|
|
2755
|
+
|
|
2756
|
+
class MusicRoomChooseOption2(TypedDict):
|
|
2757
|
+
"""Demo gate answer"""
|
|
2758
|
+
gate: Literal['demo']
|
|
2759
|
+
approve: Literal[True]
|
|
2760
|
+
|
|
2761
|
+
|
|
2762
|
+
class MusicRoomChooseOption3(TypedDict):
|
|
2763
|
+
"""Final gate answer"""
|
|
2764
|
+
gate: Literal['final']
|
|
2765
|
+
approve: Literal[True]
|
|
2766
|
+
|
|
2767
|
+
|
|
2768
|
+
# The answer to the gate the song waits at.
|
|
2769
|
+
MusicRoomChoose = Union[MusicRoomChooseOption1, MusicRoomChooseOption2, MusicRoomChooseOption3]
|
|
2770
|
+
|
|
2771
|
+
|
|
2772
|
+
class _MusicRoomDirectRequired(TypedDict):
|
|
2773
|
+
role: MusicRole
|
|
2774
|
+
direction: str
|
|
2775
|
+
|
|
2776
|
+
|
|
2777
|
+
class MusicRoomDirect(_MusicRoomDirectRequired, total=False):
|
|
2778
|
+
"""Music Room direction"""
|
|
2779
|
+
section_id: str
|
|
2780
|
+
|
|
2781
|
+
|
|
2782
|
+
class _MusicRoomListRequired(TypedDict):
|
|
2783
|
+
object: Literal['list']
|
|
2784
|
+
data: List[MusicRoom]
|
|
2785
|
+
|
|
2786
|
+
|
|
2787
|
+
class MusicRoomList(_MusicRoomListRequired, total=False):
|
|
2788
|
+
"""Music Room song list"""
|
|
2789
|
+
has_more: bool
|
|
2790
|
+
|
|
2791
|
+
|
|
2565
2792
|
class CreativeTemplateList(TypedDict):
|
|
2566
2793
|
"""Creative template list"""
|
|
2567
2794
|
object: Literal['list']
|
|
@@ -3002,6 +3229,30 @@ class StudioReviewReplyRequest(TypedDict):
|
|
|
3002
3229
|
body: str
|
|
3003
3230
|
|
|
3004
3231
|
|
|
3232
|
+
class StudioVoiceSingingConsentRequest(TypedDict):
|
|
3233
|
+
session_id: str
|
|
3234
|
+
|
|
3235
|
+
|
|
3236
|
+
class StudioVoiceSinging(TypedDict):
|
|
3237
|
+
"""Singing consent (beta)"""
|
|
3238
|
+
status: Literal['none', 'active', 'revoked']
|
|
3239
|
+
consented_at: Optional[str]
|
|
3240
|
+
revoked_at: Optional[str]
|
|
3241
|
+
quality: Literal['beta']
|
|
3242
|
+
|
|
3243
|
+
|
|
3244
|
+
class StudioVoice(TypedDict):
|
|
3245
|
+
"""Custom voice"""
|
|
3246
|
+
id: str
|
|
3247
|
+
object: Literal['studio.voice']
|
|
3248
|
+
label: str
|
|
3249
|
+
locale: str
|
|
3250
|
+
kind: Literal['ai_clone']
|
|
3251
|
+
display_name: str
|
|
3252
|
+
created_at: str
|
|
3253
|
+
singing: StudioVoiceSinging
|
|
3254
|
+
|
|
3255
|
+
|
|
3005
3256
|
class _StudioVoiceRevocationRequestRequired(TypedDict):
|
|
3006
3257
|
email: str
|
|
3007
3258
|
voice_owner_name: str
|
|
@@ -7430,6 +7681,42 @@ CreateStudioProductionResponse = StudioProduction
|
|
|
7430
7681
|
GetStudioProductionResponse = StudioProduction
|
|
7431
7682
|
|
|
7432
7683
|
|
|
7684
|
+
# GET /v1/studio/music-room — 200 response
|
|
7685
|
+
ListStudioMusicRoomsResponse = MusicRoomList
|
|
7686
|
+
|
|
7687
|
+
|
|
7688
|
+
# POST /v1/studio/music-room — request body
|
|
7689
|
+
CreateStudioMusicRoomBody = MusicRoomCreate
|
|
7690
|
+
|
|
7691
|
+
|
|
7692
|
+
# POST /v1/studio/music-room — 202 response
|
|
7693
|
+
CreateStudioMusicRoomResponse = MusicRoom
|
|
7694
|
+
|
|
7695
|
+
|
|
7696
|
+
# GET /v1/studio/music-room/quota — 200 response
|
|
7697
|
+
GetStudioMusicRoomQuotaResponse = MusicRoomQuota
|
|
7698
|
+
|
|
7699
|
+
|
|
7700
|
+
# GET /v1/studio/music-room/{id} — 200 response
|
|
7701
|
+
GetStudioMusicRoomResponse = MusicRoom
|
|
7702
|
+
|
|
7703
|
+
|
|
7704
|
+
# POST /v1/studio/music-room/{id}/choose — request body
|
|
7705
|
+
ChooseStudioMusicRoomBody = MusicRoomChoose
|
|
7706
|
+
|
|
7707
|
+
|
|
7708
|
+
# POST /v1/studio/music-room/{id}/choose — 200 response
|
|
7709
|
+
ChooseStudioMusicRoomResponse = MusicRoom
|
|
7710
|
+
|
|
7711
|
+
|
|
7712
|
+
# POST /v1/studio/music-room/{id}/direct — request body
|
|
7713
|
+
DirectStudioMusicRoomBody = MusicRoomDirect
|
|
7714
|
+
|
|
7715
|
+
|
|
7716
|
+
# POST /v1/studio/music-room/{id}/direct — 202 response
|
|
7717
|
+
DirectStudioMusicRoomResponse = MusicRoom
|
|
7718
|
+
|
|
7719
|
+
|
|
7433
7720
|
# GET /v1/studio/media-ops — 200 response
|
|
7434
7721
|
ListStudioMediaOpsResponse = MediaOpList
|
|
7435
7722
|
|
|
@@ -7586,6 +7873,14 @@ RemixStudioShareBody = StudioRemixRequest
|
|
|
7586
7873
|
RemixStudioShareResponse = StudioRemixResult
|
|
7587
7874
|
|
|
7588
7875
|
|
|
7876
|
+
# POST /v1/studio/voices/{id}/singing-consent — request body
|
|
7877
|
+
AddStudioVoiceSingingConsentBody = StudioVoiceSingingConsentRequest
|
|
7878
|
+
|
|
7879
|
+
|
|
7880
|
+
# POST /v1/studio/voices/{id}/singing-consent — 200 response
|
|
7881
|
+
AddStudioVoiceSingingConsentResponse = StudioVoice
|
|
7882
|
+
|
|
7883
|
+
|
|
7589
7884
|
# POST /v1/studio/voice-revocations — request body
|
|
7590
7885
|
CreateVoiceRevocationBody = StudioVoiceRevocationRequest
|
|
7591
7886
|
|
|
@@ -9458,6 +9753,8 @@ __all__ = [
|
|
|
9458
9753
|
'AddListItemResponse',
|
|
9459
9754
|
'AddOrgMemberBody',
|
|
9460
9755
|
'AddOrgMemberResponse',
|
|
9756
|
+
'AddStudioVoiceSingingConsentBody',
|
|
9757
|
+
'AddStudioVoiceSingingConsentResponse',
|
|
9461
9758
|
'AddTranscriptBookmarkBody',
|
|
9462
9759
|
'AddTranscriptBookmarkResponse',
|
|
9463
9760
|
'AgentOutputKey',
|
|
@@ -9572,6 +9869,8 @@ __all__ = [
|
|
|
9572
9869
|
'ChatUsage',
|
|
9573
9870
|
'ChatUsagePromptTokensDetails',
|
|
9574
9871
|
'CheckProvenanceResponse',
|
|
9872
|
+
'ChooseStudioMusicRoomBody',
|
|
9873
|
+
'ChooseStudioMusicRoomResponse',
|
|
9575
9874
|
'ClaimScheduleRunResponse',
|
|
9576
9875
|
'ClaimSessionLeaseBody',
|
|
9577
9876
|
'ClaimSessionLeaseResponse',
|
|
@@ -9687,6 +9986,8 @@ __all__ = [
|
|
|
9687
9986
|
'CreateStudioBatchBody',
|
|
9688
9987
|
'CreateStudioBatchResponse',
|
|
9689
9988
|
'CreateStudioBatchResponse201',
|
|
9989
|
+
'CreateStudioMusicRoomBody',
|
|
9990
|
+
'CreateStudioMusicRoomResponse',
|
|
9690
9991
|
'CreateStudioProductionBody',
|
|
9691
9992
|
'CreateStudioProductionResponse',
|
|
9692
9993
|
'CreateSystemOneBody',
|
|
@@ -9862,6 +10163,8 @@ __all__ = [
|
|
|
9862
10163
|
'DevicePatch',
|
|
9863
10164
|
'DevicePatched',
|
|
9864
10165
|
'DevicePlatform',
|
|
10166
|
+
'DirectStudioMusicRoomBody',
|
|
10167
|
+
'DirectStudioMusicRoomResponse',
|
|
9865
10168
|
'DispatchToDeviceBody',
|
|
9866
10169
|
'DispatchToDeviceResponse',
|
|
9867
10170
|
'DocBlock',
|
|
@@ -10007,6 +10310,8 @@ __all__ = [
|
|
|
10007
10310
|
'GetStudioBatchResponse',
|
|
10008
10311
|
'GetStudioExportResponse',
|
|
10009
10312
|
'GetStudioMediaOpJobResponse',
|
|
10313
|
+
'GetStudioMusicRoomQuotaResponse',
|
|
10314
|
+
'GetStudioMusicRoomResponse',
|
|
10010
10315
|
'GetStudioProductionResponse',
|
|
10011
10316
|
'GetStudioQuoteResponse',
|
|
10012
10317
|
'GetTaskResponse',
|
|
@@ -10102,6 +10407,7 @@ __all__ = [
|
|
|
10102
10407
|
'ListSessionsSharedWithMeResponse',
|
|
10103
10408
|
'ListStudioBatchesResponse',
|
|
10104
10409
|
'ListStudioMediaOpsResponse',
|
|
10410
|
+
'ListStudioMusicRoomsResponse',
|
|
10105
10411
|
'ListTasksResponse',
|
|
10106
10412
|
'ListTranscriptFoldersResponse',
|
|
10107
10413
|
'ListTranscriptSharesResponse',
|
|
@@ -10161,6 +10467,31 @@ __all__ = [
|
|
|
10161
10467
|
'MsgToolChoiceOption1',
|
|
10162
10468
|
'MsgToolChoiceOption2',
|
|
10163
10469
|
'MsgUsage',
|
|
10470
|
+
'MusicRole',
|
|
10471
|
+
'MusicRoom',
|
|
10472
|
+
'MusicRoomChoose',
|
|
10473
|
+
'MusicRoomChooseOption1',
|
|
10474
|
+
'MusicRoomChooseOption2',
|
|
10475
|
+
'MusicRoomChooseOption3',
|
|
10476
|
+
'MusicRoomCopyRisk',
|
|
10477
|
+
'MusicRoomCopyRiskMatchesItem',
|
|
10478
|
+
'MusicRoomCreate',
|
|
10479
|
+
'MusicRoomCritique',
|
|
10480
|
+
'MusicRoomCritiqueIssuesItem',
|
|
10481
|
+
'MusicRoomDirect',
|
|
10482
|
+
'MusicRoomGate',
|
|
10483
|
+
'MusicRoomList',
|
|
10484
|
+
'MusicRoomListening',
|
|
10485
|
+
'MusicRoomListeningFeatures',
|
|
10486
|
+
'MusicRoomListeningFeaturesAesthetics',
|
|
10487
|
+
'MusicRoomListeningVerdict',
|
|
10488
|
+
'MusicRoomListeningVerdictImpressions',
|
|
10489
|
+
'MusicRoomListeningVerdictNotesItem',
|
|
10490
|
+
'MusicRoomPlanVariant',
|
|
10491
|
+
'MusicRoomPlanVariantInstrumentationItem',
|
|
10492
|
+
'MusicRoomPlanVariantSectionsItem',
|
|
10493
|
+
'MusicRoomQuota',
|
|
10494
|
+
'MusicRoomTeamItem',
|
|
10164
10495
|
'MusicTrackMeta',
|
|
10165
10496
|
'Notification',
|
|
10166
10497
|
'OrgAddMemberBody',
|
|
@@ -10537,8 +10868,11 @@ __all__ = [
|
|
|
10537
10868
|
'StudioTimelineScene',
|
|
10538
10869
|
'StudioTimelineSceneInput',
|
|
10539
10870
|
'StudioTimelineSceneInputNarration',
|
|
10871
|
+
'StudioVoice',
|
|
10540
10872
|
'StudioVoiceRevocation',
|
|
10541
10873
|
'StudioVoiceRevocationRequest',
|
|
10874
|
+
'StudioVoiceSinging',
|
|
10875
|
+
'StudioVoiceSingingConsentRequest',
|
|
10542
10876
|
'SubmitFeedbackBody',
|
|
10543
10877
|
'SubmitFeedbackResponse',
|
|
10544
10878
|
'SubmitSessionControlBody',
|
|
@@ -8,6 +8,7 @@ from .chat import Chat, ChatCompletions, ChatResult, ChatStream, ChatStreamResul
|
|
|
8
8
|
from .brand_kits import BrandKits
|
|
9
9
|
from .templates import Templates
|
|
10
10
|
from .productions import PRODUCTION_TERMINAL_STATUSES, Productions
|
|
11
|
+
from .music_room import MUSIC_ROOM_TERMINAL_STATUSES, MusicRoom, Studio
|
|
11
12
|
from .media_ops import MEDIA_JOB_TERMINAL_STATUSES, MediaOps, is_media_op_job
|
|
12
13
|
from .lyrics import Lyrics
|
|
13
14
|
from .exports import Exports
|
|
@@ -16,6 +17,7 @@ from .vault import Vault, vault_unlock_message
|
|
|
16
17
|
from .batches import Batches
|
|
17
18
|
from .shares import Shares, share_token_from
|
|
18
19
|
from .voice_revocations import VoiceRevocations
|
|
20
|
+
from .voices import Voices
|
|
19
21
|
from .abuse_reports import AbuseReports
|
|
20
22
|
from .provenance import Provenance
|
|
21
23
|
from .creatives import Creatives, is_paged_build
|
|
@@ -124,6 +126,9 @@ __all__ = [
|
|
|
124
126
|
"Templates",
|
|
125
127
|
"Productions",
|
|
126
128
|
"PRODUCTION_TERMINAL_STATUSES",
|
|
129
|
+
"MusicRoom",
|
|
130
|
+
"Studio",
|
|
131
|
+
"MUSIC_ROOM_TERMINAL_STATUSES",
|
|
127
132
|
"MediaOps",
|
|
128
133
|
"Lyrics",
|
|
129
134
|
"MEDIA_JOB_TERMINAL_STATUSES",
|
|
@@ -134,6 +139,7 @@ __all__ = [
|
|
|
134
139
|
"Shares",
|
|
135
140
|
"share_token_from",
|
|
136
141
|
"VoiceRevocations",
|
|
142
|
+
"Voices",
|
|
137
143
|
"Vault",
|
|
138
144
|
"vault_unlock_message",
|
|
139
145
|
"AbuseReports",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""``/v1/studio/music-room`` — codai Studio Music Room (MR-09..MR-17): a team of role agents (producer, composer,
|
|
2
|
+
lyricist, arranger, session players, vocal producer, mix and mastering engineers, critic, rights) writes, plays, sings
|
|
3
|
+
and mixes a song that sounds played by people, saved as an editable audio creative.
|
|
4
|
+
|
|
5
|
+
``create`` starts a song (202), ``get`` advances and reads it, ``list`` pages your songs, ``quota`` reports the songs
|
|
6
|
+
included this month, ``choose`` answers the open gate (``plan`` / ``demo`` / ``final``) and ``direct`` asks one player
|
|
7
|
+
to re-play a part. ``wait`` polls until ``succeeded`` / ``failed`` or, with ``until_gate=True``, until the song waits
|
|
8
|
+
at a gate. Free within the monthly plan quota; beyond it every step is held on the wallet up to ``budget_micro_usd``
|
|
9
|
+
(default 4,000,000 = $4).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from .._http import RequestExtensions
|
|
18
|
+
from ._base import Resource, enc
|
|
19
|
+
|
|
20
|
+
__all__ = ["MusicRoom", "Studio", "MUSIC_ROOM_TERMINAL_STATUSES"]
|
|
21
|
+
|
|
22
|
+
_BASE = "/v1/studio/music-room"
|
|
23
|
+
MUSIC_ROOM_TERMINAL_STATUSES = ("succeeded", "failed")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _idem(key: Optional[str], ext: Optional[RequestExtensions]) -> Optional[Dict[str, Any]]:
|
|
27
|
+
if not key:
|
|
28
|
+
return dict(ext) if ext else None
|
|
29
|
+
merged: Dict[str, Any] = dict(ext or {})
|
|
30
|
+
merged["headers"] = {**dict(merged.get("headers") or {}), "Idempotency-Key": key}
|
|
31
|
+
return merged
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MusicRoom(Resource):
|
|
35
|
+
"""Start, steer and wait for Music Room songs."""
|
|
36
|
+
|
|
37
|
+
def create(
|
|
38
|
+
self,
|
|
39
|
+
brief: str,
|
|
40
|
+
language: Optional[str] = None,
|
|
41
|
+
genre: Optional[str] = None,
|
|
42
|
+
duration_seconds: Optional[int] = None,
|
|
43
|
+
vocals: Optional[bool] = None,
|
|
44
|
+
timbre: Optional[str] = None,
|
|
45
|
+
voice_passport_id: Optional[str] = None,
|
|
46
|
+
instruments: Optional[List[str]] = None,
|
|
47
|
+
auto: Optional[bool] = None,
|
|
48
|
+
budget_micro_usd: Optional[int] = None,
|
|
49
|
+
title: Optional[str] = None,
|
|
50
|
+
idempotency_key: Optional[str] = None,
|
|
51
|
+
ext: Optional[RequestExtensions] = None,
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
"""``POST /v1/studio/music-room`` — start a song (the producer plans it, then it waits at the plan gate)."""
|
|
54
|
+
body: Dict[str, Any] = {"brief": brief}
|
|
55
|
+
for k, v in (
|
|
56
|
+
("language", language),
|
|
57
|
+
("genre", genre),
|
|
58
|
+
("duration_seconds", duration_seconds),
|
|
59
|
+
("vocals", vocals),
|
|
60
|
+
("timbre", timbre),
|
|
61
|
+
("voice_passport_id", voice_passport_id),
|
|
62
|
+
("instruments", instruments),
|
|
63
|
+
("auto", auto),
|
|
64
|
+
("budget_micro_usd", budget_micro_usd),
|
|
65
|
+
("title", title),
|
|
66
|
+
):
|
|
67
|
+
if v is not None:
|
|
68
|
+
body[k] = v
|
|
69
|
+
return (
|
|
70
|
+
self._json(
|
|
71
|
+
_BASE,
|
|
72
|
+
ext=_idem(idempotency_key, ext),
|
|
73
|
+
method="POST",
|
|
74
|
+
body=body,
|
|
75
|
+
no_retry=not idempotency_key,
|
|
76
|
+
)
|
|
77
|
+
or {}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def get(self, music_room_id: str, ext: Optional[RequestExtensions] = None) -> Dict[str, Any]:
|
|
81
|
+
"""``GET /v1/studio/music-room/{id}`` — advances the workflow and returns the song. Another account's id is a 404."""
|
|
82
|
+
return self._json(f"{_BASE}/{enc(music_room_id)}", ext=ext) or {}
|
|
83
|
+
|
|
84
|
+
def list(
|
|
85
|
+
self,
|
|
86
|
+
limit: Optional[int] = None,
|
|
87
|
+
before: Optional[str] = None,
|
|
88
|
+
ext: Optional[RequestExtensions] = None,
|
|
89
|
+
) -> Dict[str, Any]:
|
|
90
|
+
"""``GET /v1/studio/music-room`` — ``{object: "list", data, has_more}``, newest first; page with ``before``."""
|
|
91
|
+
return self._json(_BASE, ext=ext, query={"limit": limit, "before": before}) or {}
|
|
92
|
+
|
|
93
|
+
def quota(self, ext: Optional[RequestExtensions] = None) -> Dict[str, Any]:
|
|
94
|
+
"""``GET /v1/studio/music-room/quota`` — ``{plan, included, used, remaining, period_end}`` for this month."""
|
|
95
|
+
return self._json(f"{_BASE}/quota", ext=ext) or {}
|
|
96
|
+
|
|
97
|
+
def choose(
|
|
98
|
+
self,
|
|
99
|
+
music_room_id: str,
|
|
100
|
+
gate: str,
|
|
101
|
+
variant: Optional[int] = None,
|
|
102
|
+
approve: Optional[bool] = None,
|
|
103
|
+
ext: Optional[RequestExtensions] = None,
|
|
104
|
+
) -> Dict[str, Any]:
|
|
105
|
+
"""``POST /v1/studio/music-room/{id}/choose`` — answer the open gate: ``gate="plan", variant=0..2`` or
|
|
106
|
+
``gate="demo"`` / ``gate="final"`` (approved). 409 ``not_at_gate`` when the song is not waiting there."""
|
|
107
|
+
body: Dict[str, Any] = {"gate": gate}
|
|
108
|
+
if gate == "plan":
|
|
109
|
+
body["variant"] = 0 if variant is None else variant
|
|
110
|
+
else:
|
|
111
|
+
body["approve"] = True if approve is None else approve
|
|
112
|
+
return self._json(f"{_BASE}/{enc(music_room_id)}/choose", ext=ext, method="POST", body=body) or {}
|
|
113
|
+
|
|
114
|
+
def direct(
|
|
115
|
+
self,
|
|
116
|
+
music_room_id: str,
|
|
117
|
+
role: str,
|
|
118
|
+
direction: str,
|
|
119
|
+
section_id: Optional[str] = None,
|
|
120
|
+
idempotency_key: Optional[str] = None,
|
|
121
|
+
ext: Optional[RequestExtensions] = None,
|
|
122
|
+
) -> Dict[str, Any]:
|
|
123
|
+
"""``POST /v1/studio/music-room/{id}/direct`` — one player re-plays its part following ``direction`` (≤ 500
|
|
124
|
+
characters) and the demo is re-rendered. Only at the demo or final gate."""
|
|
125
|
+
body: Dict[str, Any] = {"role": role, "direction": direction}
|
|
126
|
+
if section_id is not None:
|
|
127
|
+
body["section_id"] = section_id
|
|
128
|
+
return (
|
|
129
|
+
self._json(
|
|
130
|
+
f"{_BASE}/{enc(music_room_id)}/direct",
|
|
131
|
+
ext=_idem(idempotency_key, ext),
|
|
132
|
+
method="POST",
|
|
133
|
+
body=body,
|
|
134
|
+
no_retry=not idempotency_key,
|
|
135
|
+
)
|
|
136
|
+
or {}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def wait(
|
|
140
|
+
self,
|
|
141
|
+
music_room_id: str,
|
|
142
|
+
until_gate: bool = False,
|
|
143
|
+
interval: float = 10.0,
|
|
144
|
+
timeout: float = 1800.0,
|
|
145
|
+
ext: Optional[RequestExtensions] = None,
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
"""Poll :meth:`get` until ``status`` is ``succeeded`` or ``failed`` (a failed song is returned too) or, with
|
|
148
|
+
``until_gate=True``, until it waits at a gate. Raises ``TimeoutError`` after ``timeout`` seconds (default 30 min)."""
|
|
149
|
+
deadline = time.monotonic() + timeout
|
|
150
|
+
while True:
|
|
151
|
+
r = self.get(music_room_id, ext=ext)
|
|
152
|
+
status = r.get("status")
|
|
153
|
+
if status in MUSIC_ROOM_TERMINAL_STATUSES or (until_gate and status == "waiting"):
|
|
154
|
+
return r
|
|
155
|
+
if time.monotonic() + interval > deadline:
|
|
156
|
+
raise TimeoutError(f"codai music room {music_room_id} did not finish within {timeout} s")
|
|
157
|
+
time.sleep(interval)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Studio:
|
|
161
|
+
"""codai Studio namespace: ``client.studio.music_room``."""
|
|
162
|
+
|
|
163
|
+
def __init__(self, http: Any) -> None:
|
|
164
|
+
self.music_room = MusicRoom(http)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""``client.voices`` — codai Studio custom voices, D37 voice passport (beta): the SEPARATE, revocable consent that
|
|
2
|
+
lets your own custom voice sing in your Music Room songs. The consent is recorded in the app (a ``purpose: singing``
|
|
3
|
+
recording session: the singing statement read aloud + a sung sample); these calls arm and revoke it. The sung result
|
|
4
|
+
approximates your timbre; it is not a faithful clone. Only where voice cloning is enabled for the account. Free."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from .._http import RequestExtensions
|
|
11
|
+
from ._base import Resource, enc
|
|
12
|
+
|
|
13
|
+
__all__ = ["Voices"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _singing(voice_id: str) -> str:
|
|
17
|
+
return f"/v1/studio/voices/{enc(voice_id)}/singing-consent"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Voices(Resource):
|
|
21
|
+
"""Custom voices: the voice passport singing consent."""
|
|
22
|
+
|
|
23
|
+
def add_singing_consent(
|
|
24
|
+
self, voice_id: str, session_id: str, ext: Optional[RequestExtensions] = None
|
|
25
|
+
) -> Dict[str, Any]:
|
|
26
|
+
"""``POST /v1/studio/voices/{id}/singing-consent`` — add the singing purpose from a finished singing recording
|
|
27
|
+
session (liveness-checked statement + sung sample). Returns the voice with ``singing.status == "active"``.
|
|
28
|
+
Never retried."""
|
|
29
|
+
return (
|
|
30
|
+
self._http.request(
|
|
31
|
+
_singing(voice_id), ext=ext, method="POST", body={"session_id": session_id}, no_retry=True
|
|
32
|
+
).json()
|
|
33
|
+
or {}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def revoke_singing_consent(self, voice_id: str, ext: Optional[RequestExtensions] = None) -> None:
|
|
37
|
+
"""``DELETE /v1/studio/voices/{id}/singing-consent`` — revoke ONLY the singing purpose (204). The sung sample is
|
|
38
|
+
deleted at once; the voice keeps working for narration. Idempotent."""
|
|
39
|
+
self._http.request(_singing(voice_id), ext=ext, method="DELETE")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""``client.studio.music_room`` (Studio Music Room, MR-09..MR-17) against the http.server stub."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
_ID = "7e2f3a4b-5c6d-4e7f-8091-a2b3c4d5e6f7"
|
|
8
|
+
_BASE = "/v1/studio/music-room"
|
|
9
|
+
_RUNNING = {"id": _ID, "object": "studio.music_room", "status": "running", "phase": "plan", "gate": None}
|
|
10
|
+
_AT_PLAN = {**_RUNNING, "status": "waiting", "phase": "gate_plan", "gate": {"kind": "plan", "since": "2026-10-22T00:01:00Z"}}
|
|
11
|
+
_DONE = {**_RUNNING, "status": "succeeded", "phase": "ready", "final_asset_id": "a9"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_create_posts_only_set_fields_and_idempotency_key(client, stub):
|
|
15
|
+
stub.reply(202, _RUNNING)
|
|
16
|
+
r = client.studio.music_room.create(
|
|
17
|
+
"o manea veselă de nuntă", language="ro", genre="manele", vocals=False, instruments=["balkan.accordion"],
|
|
18
|
+
idempotency_key="mr-1",
|
|
19
|
+
)
|
|
20
|
+
assert r["object"] == "studio.music_room"
|
|
21
|
+
req = stub.requests[0]
|
|
22
|
+
assert (req.method, req.path) == ("POST", _BASE)
|
|
23
|
+
assert req.json == {
|
|
24
|
+
"brief": "o manea veselă de nuntă",
|
|
25
|
+
"language": "ro",
|
|
26
|
+
"genre": "manele",
|
|
27
|
+
"vocals": False,
|
|
28
|
+
"instruments": ["balkan.accordion"],
|
|
29
|
+
}
|
|
30
|
+
assert req.headers["idempotency-key"] == "mr-1"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_get_encodes_id_and_quota_and_list_paths(client, stub):
|
|
34
|
+
stub.reply(200, _AT_PLAN)
|
|
35
|
+
stub.reply(200, {"plan": "pro", "included": 4, "used": 1, "remaining": 3, "period_end": "2026-11-01T00:00:00Z"})
|
|
36
|
+
stub.reply(200, {"object": "list", "data": [_RUNNING], "has_more": False})
|
|
37
|
+
stub.reply(200, {"object": "list", "data": []})
|
|
38
|
+
assert client.studio.music_room.get("a/b")["gate"]["kind"] == "plan"
|
|
39
|
+
assert client.studio.music_room.quota()["remaining"] == 3
|
|
40
|
+
assert len(client.studio.music_room.list(limit=5, before="2026-10-01T00:00:00Z")["data"]) == 1
|
|
41
|
+
client.studio.music_room.list()
|
|
42
|
+
assert [(r.method, r.path, r.query) for r in stub.requests] == [
|
|
43
|
+
("GET", f"{_BASE}/a%2Fb", ""),
|
|
44
|
+
("GET", f"{_BASE}/quota", ""),
|
|
45
|
+
("GET", _BASE, "limit=5&before=2026-10-01T00%3A00%3A00Z"),
|
|
46
|
+
("GET", _BASE, ""),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_choose_builds_each_gate_answer(client, stub):
|
|
51
|
+
for _ in range(3):
|
|
52
|
+
stub.reply(200, _RUNNING)
|
|
53
|
+
client.studio.music_room.choose(_ID, "plan", variant=2)
|
|
54
|
+
client.studio.music_room.choose(_ID, "demo")
|
|
55
|
+
client.studio.music_room.choose(_ID, "final", approve=True)
|
|
56
|
+
assert [(r.method, r.path) for r in stub.requests] == [("POST", f"{_BASE}/{_ID}/choose")] * 3
|
|
57
|
+
assert [r.json for r in stub.requests] == [
|
|
58
|
+
{"gate": "plan", "variant": 2},
|
|
59
|
+
{"gate": "demo", "approve": True},
|
|
60
|
+
{"gate": "final", "approve": True},
|
|
61
|
+
]
|
|
62
|
+
assert "idempotency-key" not in stub.requests[0].headers
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_direct_posts_direction_with_idempotency_key(client, stub):
|
|
66
|
+
stub.reply(202, _RUNNING)
|
|
67
|
+
client.studio.music_room.direct(_ID, "drums", "fewer fills", section_id="chorus1", idempotency_key="d1")
|
|
68
|
+
req = stub.requests[0]
|
|
69
|
+
assert (req.method, req.path) == ("POST", f"{_BASE}/{_ID}/direct")
|
|
70
|
+
assert req.json == {"role": "drums", "direction": "fewer fills", "section_id": "chorus1"}
|
|
71
|
+
assert req.headers["idempotency-key"] == "d1"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_wait_polls_past_gates_until_succeeded(client, stub):
|
|
75
|
+
stub.reply(200, _RUNNING)
|
|
76
|
+
stub.reply(200, _AT_PLAN)
|
|
77
|
+
stub.reply(200, _DONE)
|
|
78
|
+
assert client.studio.music_room.wait(_ID, interval=0.01)["final_asset_id"] == "a9"
|
|
79
|
+
assert len(stub.requests) == 3
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_wait_until_gate_returns_at_the_first_gate(client, stub):
|
|
83
|
+
stub.reply(200, _RUNNING)
|
|
84
|
+
stub.reply(200, _AT_PLAN)
|
|
85
|
+
stub.reply(200, _DONE)
|
|
86
|
+
r = client.studio.music_room.wait(_ID, until_gate=True, interval=0.01)
|
|
87
|
+
assert r["status"] == "waiting" and r["gate"]["kind"] == "plan"
|
|
88
|
+
assert len(stub.requests) == 2
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_wait_times_out(client, stub):
|
|
92
|
+
stub.reply(200, _RUNNING)
|
|
93
|
+
with pytest.raises(TimeoutError):
|
|
94
|
+
client.studio.music_room.wait(_ID, interval=0.05, timeout=0.01)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Voice passport singing consent (D37, beta) against the http.server stub."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
_VOICE = "7a1b2c3d-0000-4000-8000-0000000000a1"
|
|
6
|
+
_SESSION = "7a1b2c3d-0000-4000-8000-0000000000b2"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_add_singing_consent_posts_the_session(client, stub):
|
|
10
|
+
stub.reply(
|
|
11
|
+
200,
|
|
12
|
+
{
|
|
13
|
+
"id": _VOICE,
|
|
14
|
+
"object": "studio.voice",
|
|
15
|
+
"singing": {"status": "active", "consented_at": "2026-10-22T10:05:00.000Z", "revoked_at": None,
|
|
16
|
+
"quality": "beta"},
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
|
+
v = client.voices.add_singing_consent(_VOICE, _SESSION)
|
|
20
|
+
assert v["singing"]["status"] == "active" and v["singing"]["quality"] == "beta"
|
|
21
|
+
assert [(q.method, q.path) for q in stub.requests] == [("POST", f"/v1/studio/voices/{_VOICE}/singing-consent")]
|
|
22
|
+
assert stub.last.json == {"session_id": _SESSION}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_revoke_singing_consent_deletes(client, stub):
|
|
26
|
+
stub.reply(204)
|
|
27
|
+
assert client.voices.revoke_singing_consent(_VOICE) is None
|
|
28
|
+
assert [(q.method, q.path) for q in stub.requests] == [("DELETE", f"/v1/studio/voices/{_VOICE}/singing-consent")]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|