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.
Files changed (99) hide show
  1. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/CHANGELOG.md +24 -3
  2. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/PKG-INFO +1 -1
  3. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/pyproject.toml +1 -1
  4. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/__init__.py +7 -1
  5. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/_operations.py +8 -0
  6. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/_types.py +334 -0
  7. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/__init__.py +6 -0
  8. codai_sdk-0.69.0/src/codai/resources/music_room.py +164 -0
  9. codai_sdk-0.69.0/src/codai/resources/voices.py +39 -0
  10. codai_sdk-0.69.0/tests/test_music_room.py +94 -0
  11. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_resources.py +1 -1
  12. codai_sdk-0.69.0/tests/test_voices.py +28 -0
  13. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  15. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/dependabot.yml +0 -0
  16. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/pull_request_template.md +0 -0
  17. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.github/workflows/publish.yml +0 -0
  18. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/.gitignore +0 -0
  19. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/CODE_OF_CONDUCT.md +0 -0
  20. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/CONTRIBUTING.md +0 -0
  21. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/LICENSE +0 -0
  22. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/README.md +0 -0
  23. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/SECURITY.md +0 -0
  24. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/examples/agent.py +0 -0
  25. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/examples/chat.py +0 -0
  26. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/examples/feedback.py +0 -0
  27. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/examples/streaming.py +0 -0
  28. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/scripts/gen-types.py +0 -0
  29. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/scripts/smoke_local.py +0 -0
  30. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/_http.py +0 -0
  31. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/_resolve_types.py +0 -0
  32. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/_base.py +0 -0
  33. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/abuse_reports.py +0 -0
  34. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/agents.py +0 -0
  35. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/batches.py +0 -0
  36. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/billing.py +0 -0
  37. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/brand_kits.py +0 -0
  38. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/cards.py +0 -0
  39. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/chat.py +0 -0
  40. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/core.py +0 -0
  41. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/creatives.py +0 -0
  42. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/environments.py +0 -0
  43. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/exports.py +0 -0
  44. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/lists.py +0 -0
  45. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/lyrics.py +0 -0
  46. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/media_ops.py +0 -0
  47. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/memory.py +0 -0
  48. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/messages.py +0 -0
  49. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/platform.py +0 -0
  50. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/productions.py +0 -0
  51. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/prompts.py +0 -0
  52. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/provenance.py +0 -0
  53. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/referrals.py +0 -0
  54. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/research.py +0 -0
  55. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/resolve.py +0 -0
  56. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/responses.py +0 -0
  57. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/sandboxes.py +0 -0
  58. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/schedules.py +0 -0
  59. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/sessions.py +0 -0
  60. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/shares.py +0 -0
  61. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/templates.py +0 -0
  62. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/transcripts.py +0 -0
  63. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/triggers.py +0 -0
  64. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/usage_keys.py +0 -0
  65. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/vault.py +0 -0
  66. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/src/codai/resources/voice_revocations.py +0 -0
  67. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/conftest.py +0 -0
  68. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_abuse_reports.py +0 -0
  69. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_batches.py +0 -0
  70. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_billing_referrals.py +0 -0
  71. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_brand_kits.py +0 -0
  72. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_cards.py +0 -0
  73. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives.py +0 -0
  74. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives_cx07.py +0 -0
  75. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives_documents.py +0 -0
  76. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives_e11.py +0 -0
  77. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_creatives_e11b.py +0 -0
  78. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_environments_exec.py +0 -0
  79. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_exports.py +0 -0
  80. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_http.py +0 -0
  81. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_lists.py +0 -0
  82. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_lyrics.py +0 -0
  83. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_media_ops.py +0 -0
  84. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_memory.py +0 -0
  85. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_os_gate.py +0 -0
  86. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_parity.py +0 -0
  87. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_productions.py +0 -0
  88. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_prompts.py +0 -0
  89. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_provenance.py +0 -0
  90. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_research.py +0 -0
  91. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_resolve.py +0 -0
  92. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_sandboxes.py +0 -0
  93. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_settings.py +0 -0
  94. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_shares_e12a.py +0 -0
  95. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_templates.py +0 -0
  96. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_transcripts.py +0 -0
  97. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_vault.py +0 -0
  98. {codai_sdk-0.67.0 → codai_sdk-0.69.0}/tests/test_voice_revocations.py +0 -0
  99. {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
- idempotency_key=)` (`POST /v1/studio/creatives/{id}/nodes/{nodeId}/regenerate`): new picture for an image node,
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
- rhyme_scheme=, lines_per_section=, title=, idempotency_key=)` (`POST /v1/studio/lyrics`) and `align(creative_id,
38
- clip_id, lyrics_text, language=, apply=, base_seq=, retranscribe=, idempotency_key=)` (`POST /v1/studio/lyrics/align`,
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.67.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.67.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.67.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)
@@ -24,7 +24,7 @@ def _chat_response(content="PONG", tool_calls=None):
24
24
 
25
25
 
26
26
  def test_version():
27
- assert codai.__version__ == "0.67.0"
27
+ assert codai.__version__ == "0.69.0"
28
28
  def test_chat_completions_create(client, stub):
29
29
  stub.reply(
30
30
  200,
@@ -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