venice-cli 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- venice/__init__.py +1 -0
- venice/__main__.py +6 -0
- venice/audio_player.py +59 -0
- venice/audio_post.py +274 -0
- venice/auth.py +101 -0
- venice/billing.py +129 -0
- venice/cli.py +31 -0
- venice/client.py +258 -0
- venice/commands/__init__.py +22 -0
- venice/commands/_audio.py +96 -0
- venice/commands/_models.py +83 -0
- venice/commands/_openai.py +62 -0
- venice/commands/_queue.py +87 -0
- venice/commands/_shared.py +132 -0
- venice/commands/balance.py +124 -0
- venice/commands/bg_remove.py +126 -0
- venice/commands/chat.py +263 -0
- venice/commands/contact_sheet.py +59 -0
- venice/commands/embed.py +157 -0
- venice/commands/image.py +657 -0
- venice/commands/login.py +29 -0
- venice/commands/master.py +42 -0
- venice/commands/models.py +174 -0
- venice/commands/music.py +316 -0
- venice/commands/sfx.py +199 -0
- venice/commands/tts.py +362 -0
- venice/commands/upscale.py +177 -0
- venice/commands/video.py +298 -0
- venice/config.py +19 -0
- venice/image_montage.py +230 -0
- venice_cli-0.14.0.dist-info/METADATA +654 -0
- venice_cli-0.14.0.dist-info/RECORD +35 -0
- venice_cli-0.14.0.dist-info/WHEEL +4 -0
- venice_cli-0.14.0.dist-info/entry_points.txt +2 -0
- venice_cli-0.14.0.dist-info/licenses/LICENSE +21 -0
venice/commands/sfx.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""`venice sfx` -- generate a sound effect via Venice's async audio queue."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .. import audio_post, billing, config
|
|
8
|
+
from ..client import VeniceAPIError
|
|
9
|
+
from . import _audio, _queue, _shared
|
|
10
|
+
|
|
11
|
+
SFX_MODELS = {
|
|
12
|
+
"elevenlabs-sound-effects-v2": (22, "mp3"),
|
|
13
|
+
"mmaudio-v2-text-to-audio": (30, "mp3"),
|
|
14
|
+
}
|
|
15
|
+
DEFAULT_SFX_MODEL = "elevenlabs-sound-effects-v2"
|
|
16
|
+
DEFAULT_DURATION = 5
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register(subparsers) -> None:
|
|
20
|
+
p = subparsers.add_parser(
|
|
21
|
+
"sfx",
|
|
22
|
+
help="Generate a sound effect via Venice audio queue.",
|
|
23
|
+
description=(
|
|
24
|
+
"Generates a sound effect. Async flow: quote -> queue -> poll -> "
|
|
25
|
+
"save. Use --dry-run to see only the cost quote. To fetch a "
|
|
26
|
+
"backgrounded job by its queue_id, use `venice sfx-status`."
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
p.add_argument("prompt", nargs="?", help="Sound description (e.g. 'thunderstorm rolling in').")
|
|
30
|
+
p.add_argument(
|
|
31
|
+
"--model",
|
|
32
|
+
choices=sorted(SFX_MODELS),
|
|
33
|
+
default=DEFAULT_SFX_MODEL,
|
|
34
|
+
)
|
|
35
|
+
p.add_argument("--duration", type=int, default=DEFAULT_DURATION)
|
|
36
|
+
p.add_argument("--output", "-o", type=Path, default=None)
|
|
37
|
+
play_grp = p.add_mutually_exclusive_group()
|
|
38
|
+
play_grp.add_argument("--play", dest="play", action="store_true", default=None)
|
|
39
|
+
play_grp.add_argument("--no-play", dest="play", action="store_false")
|
|
40
|
+
p.add_argument("--yes", "-y", action="store_true")
|
|
41
|
+
p.add_argument("--background", action="store_true")
|
|
42
|
+
p.add_argument("--dry-run", action="store_true")
|
|
43
|
+
p.add_argument("--no-cleanup", action="store_true")
|
|
44
|
+
p.add_argument(
|
|
45
|
+
"--max-spend",
|
|
46
|
+
type=float,
|
|
47
|
+
default=None,
|
|
48
|
+
metavar="USD",
|
|
49
|
+
help="Refuse to queue if the quote exceeds this USD cap.",
|
|
50
|
+
)
|
|
51
|
+
p.add_argument(
|
|
52
|
+
"--no-balance",
|
|
53
|
+
action="store_true",
|
|
54
|
+
help="Skip the upfront balance display.",
|
|
55
|
+
)
|
|
56
|
+
p.add_argument("--poll-interval", type=float, default=config.SFX_POLL_INTERVAL_SEC)
|
|
57
|
+
p.add_argument("--max-wait", type=float, default=config.SFX_POLL_MAX_WAIT_SEC)
|
|
58
|
+
audio_post.add_master_flags(p, include_toggle=True)
|
|
59
|
+
p.set_defaults(handler=_run_generate)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def register_status(subparsers) -> None:
|
|
63
|
+
sp = subparsers.add_parser(
|
|
64
|
+
"sfx-status",
|
|
65
|
+
help="Fetch a previously-backgrounded SFX job by queue_id.",
|
|
66
|
+
description=(
|
|
67
|
+
"Polls /audio/retrieve for an already-queued job (typically from "
|
|
68
|
+
"`venice sfx ... --background`) and downloads the audio when ready."
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
sp.add_argument("queue_id")
|
|
72
|
+
sp.add_argument(
|
|
73
|
+
"--model",
|
|
74
|
+
choices=sorted(SFX_MODELS),
|
|
75
|
+
default=DEFAULT_SFX_MODEL,
|
|
76
|
+
)
|
|
77
|
+
sp.add_argument("--output", "-o", type=Path, default=None)
|
|
78
|
+
sp.add_argument("--play", action="store_true")
|
|
79
|
+
sp.add_argument("--no-cleanup", action="store_true")
|
|
80
|
+
sp.add_argument("--poll-interval", type=float, default=config.SFX_POLL_INTERVAL_SEC)
|
|
81
|
+
sp.add_argument("--max-wait", type=float, default=config.SFX_POLL_MAX_WAIT_SEC)
|
|
82
|
+
sp.set_defaults(handler=_run_status)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _clamp_duration(model: str, duration: int) -> int:
|
|
86
|
+
max_dur, _ = SFX_MODELS[model]
|
|
87
|
+
if duration <= 0:
|
|
88
|
+
print(f"sfx: --duration must be > 0; using {DEFAULT_DURATION}", file=sys.stderr)
|
|
89
|
+
return DEFAULT_DURATION
|
|
90
|
+
if duration > max_dur:
|
|
91
|
+
print(
|
|
92
|
+
f"sfx: --duration {duration}s exceeds {model} max {max_dur}s; clamping",
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
return max_dur
|
|
96
|
+
return duration
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _run_generate(args) -> int:
|
|
100
|
+
if not args.prompt:
|
|
101
|
+
print("sfx: prompt required (or use: venice sfx-status <id>)", file=sys.stderr)
|
|
102
|
+
return 2
|
|
103
|
+
|
|
104
|
+
if args.master and not audio_post.has_ffmpeg():
|
|
105
|
+
print("sfx: --master requires ffmpeg on PATH; install it or drop --master",
|
|
106
|
+
file=sys.stderr)
|
|
107
|
+
return 2
|
|
108
|
+
|
|
109
|
+
client, rc = _queue.build_client()
|
|
110
|
+
if rc != 0:
|
|
111
|
+
return rc
|
|
112
|
+
|
|
113
|
+
duration = _clamp_duration(args.model, args.duration)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
quote = client.post_json(
|
|
117
|
+
"/audio/quote",
|
|
118
|
+
{"model": args.model, "duration_seconds": duration},
|
|
119
|
+
)
|
|
120
|
+
except VeniceAPIError as e:
|
|
121
|
+
print(f"quote rejected: {e}", file=sys.stderr)
|
|
122
|
+
return _queue.status_to_exit(e)
|
|
123
|
+
|
|
124
|
+
quote_value = quote.get("quote", quote)
|
|
125
|
+
_shared.print_estimate(quote_value, f"model={args.model}, duration={duration}s")
|
|
126
|
+
_shared.print_balance_and_remaining(client, quote_value, show=not args.no_balance)
|
|
127
|
+
|
|
128
|
+
if _shared.over_budget(quote_value, args.max_spend):
|
|
129
|
+
print(
|
|
130
|
+
f"sfx: quote {billing.format_usd(quote_value)} exceeds "
|
|
131
|
+
f"--max-spend {billing.format_usd(args.max_spend)}; aborting",
|
|
132
|
+
file=sys.stderr,
|
|
133
|
+
)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
if args.dry_run:
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
if not args.background:
|
|
140
|
+
rc = _shared.confirm_or_exit(args.yes)
|
|
141
|
+
if rc is not None:
|
|
142
|
+
return rc
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
queued = client.post_json(
|
|
146
|
+
"/audio/queue",
|
|
147
|
+
{"model": args.model, "prompt": args.prompt, "duration_seconds": duration},
|
|
148
|
+
)
|
|
149
|
+
except VeniceAPIError as e:
|
|
150
|
+
print(f"queue failed: {e}", file=sys.stderr)
|
|
151
|
+
return _queue.status_to_exit(e)
|
|
152
|
+
|
|
153
|
+
queue_id = queued.get("queue_id") or queued.get("id") or ""
|
|
154
|
+
if not queue_id:
|
|
155
|
+
print(f"queue response missing queue_id: {queued!r}", file=sys.stderr)
|
|
156
|
+
return 5
|
|
157
|
+
|
|
158
|
+
if args.background:
|
|
159
|
+
sys.stdout.write(queue_id + "\n")
|
|
160
|
+
sys.stdout.flush()
|
|
161
|
+
print(
|
|
162
|
+
f"queued as {queue_id}; fetch with: venice sfx-status {queue_id}",
|
|
163
|
+
file=sys.stderr,
|
|
164
|
+
)
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
post = audio_post.master_hook(args) if args.master else None
|
|
168
|
+
return _audio.retrieve_and_save(
|
|
169
|
+
client,
|
|
170
|
+
args.model,
|
|
171
|
+
queue_id,
|
|
172
|
+
args.output,
|
|
173
|
+
args.poll_interval,
|
|
174
|
+
args.max_wait,
|
|
175
|
+
args.no_cleanup,
|
|
176
|
+
args.play,
|
|
177
|
+
name_prefix="venice-sfx",
|
|
178
|
+
retry_hint=f"venice sfx-status {queue_id}",
|
|
179
|
+
post_process=post,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _run_status(args) -> int:
|
|
184
|
+
client, rc = _queue.build_client()
|
|
185
|
+
if rc != 0:
|
|
186
|
+
return rc
|
|
187
|
+
want_play = True if args.play else None
|
|
188
|
+
return _audio.retrieve_and_save(
|
|
189
|
+
client,
|
|
190
|
+
args.model,
|
|
191
|
+
args.queue_id,
|
|
192
|
+
args.output,
|
|
193
|
+
args.poll_interval,
|
|
194
|
+
args.max_wait,
|
|
195
|
+
args.no_cleanup,
|
|
196
|
+
want_play,
|
|
197
|
+
name_prefix="venice-sfx",
|
|
198
|
+
retry_hint=f"venice sfx-status {args.queue_id}",
|
|
199
|
+
)
|
venice/commands/tts.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""`venice tts` -- synthesize speech via Venice's /audio/speech endpoint.
|
|
2
|
+
|
|
3
|
+
Sync flow (no queue). Pricing is per 1M characters; the command fetches
|
|
4
|
+
the live per-model rate from /models?type=tts and shows the estimate
|
|
5
|
+
upfront alongside the current balance. Mirrors the SFX UX where
|
|
6
|
+
sensible (--yes, --max-spend, --no-balance, --dry-run, --output, --play).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from .. import audio_player, auth, billing, config
|
|
16
|
+
from ..client import VeniceAPIError, build_client_from_auth
|
|
17
|
+
|
|
18
|
+
# Slugs verified against /models?type=tts on 2026-05-22.
|
|
19
|
+
TTS_MODELS = (
|
|
20
|
+
"tts-kokoro",
|
|
21
|
+
"tts-qwen3-0-6b",
|
|
22
|
+
"tts-qwen3-1-7b",
|
|
23
|
+
"tts-xai-v1",
|
|
24
|
+
"tts-inworld-1-5-max",
|
|
25
|
+
"tts-chatterbox-hd",
|
|
26
|
+
"tts-orpheus",
|
|
27
|
+
"tts-elevenlabs-turbo-v2-5",
|
|
28
|
+
"tts-minimax-speech-02-hd",
|
|
29
|
+
"tts-gemini-3-1-flash",
|
|
30
|
+
)
|
|
31
|
+
DEFAULT_TTS_MODEL = "tts-kokoro" # cheapest ($3.50/1M chars), 54 voices
|
|
32
|
+
|
|
33
|
+
FORMATS = ("mp3", "opus", "aac", "flac", "wav", "pcm")
|
|
34
|
+
DEFAULT_FORMAT = "mp3"
|
|
35
|
+
|
|
36
|
+
EXT_BY_FORMAT = {
|
|
37
|
+
"mp3": ".mp3",
|
|
38
|
+
"opus": ".opus",
|
|
39
|
+
"aac": ".aac",
|
|
40
|
+
"flac": ".flac",
|
|
41
|
+
"wav": ".wav",
|
|
42
|
+
"pcm": ".pcm",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def register(subparsers) -> None:
|
|
47
|
+
p = subparsers.add_parser(
|
|
48
|
+
"tts",
|
|
49
|
+
help="Synthesize speech via /audio/speech (sync).",
|
|
50
|
+
description=(
|
|
51
|
+
"Synthesizes speech. Input from positional text, --from-file, "
|
|
52
|
+
"or --stdin. Pricing is per 1M characters; cost is estimated "
|
|
53
|
+
"from the live model rate. Use `venice models <model-slug>` to "
|
|
54
|
+
"see the voice list for a given TTS model."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
p.add_argument(
|
|
58
|
+
"text",
|
|
59
|
+
nargs="?",
|
|
60
|
+
help="Text to speak. Use '-' for stdin, or omit and pass --from-file/--stdin.",
|
|
61
|
+
)
|
|
62
|
+
src = p.add_mutually_exclusive_group()
|
|
63
|
+
src.add_argument(
|
|
64
|
+
"--from-file",
|
|
65
|
+
type=Path,
|
|
66
|
+
default=None,
|
|
67
|
+
metavar="PATH",
|
|
68
|
+
help="Read the input text from PATH instead of the positional arg.",
|
|
69
|
+
)
|
|
70
|
+
src.add_argument(
|
|
71
|
+
"--stdin",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Read the input text from stdin until EOF.",
|
|
74
|
+
)
|
|
75
|
+
p.add_argument(
|
|
76
|
+
"--model",
|
|
77
|
+
choices=TTS_MODELS,
|
|
78
|
+
default=DEFAULT_TTS_MODEL,
|
|
79
|
+
)
|
|
80
|
+
p.add_argument(
|
|
81
|
+
"--voice",
|
|
82
|
+
default=None,
|
|
83
|
+
help="Voice id (model-specific). If omitted, Venice uses the model default.",
|
|
84
|
+
)
|
|
85
|
+
p.add_argument(
|
|
86
|
+
"--format",
|
|
87
|
+
choices=FORMATS,
|
|
88
|
+
default=DEFAULT_FORMAT,
|
|
89
|
+
help=f"Output audio format (default {DEFAULT_FORMAT}).",
|
|
90
|
+
)
|
|
91
|
+
p.add_argument(
|
|
92
|
+
"--speed",
|
|
93
|
+
type=float,
|
|
94
|
+
default=None,
|
|
95
|
+
metavar="N",
|
|
96
|
+
help="Playback speed (0.25-4.0). Omit to use server default (1.0).",
|
|
97
|
+
)
|
|
98
|
+
p.add_argument("--output", "-o", type=Path, default=None)
|
|
99
|
+
play_grp = p.add_mutually_exclusive_group()
|
|
100
|
+
play_grp.add_argument("--play", dest="play", action="store_true", default=None)
|
|
101
|
+
play_grp.add_argument("--no-play", dest="play", action="store_false")
|
|
102
|
+
p.add_argument("--yes", "-y", action="store_true")
|
|
103
|
+
p.add_argument("--dry-run", action="store_true",
|
|
104
|
+
help="Estimate cost and exit; don't call /audio/speech.")
|
|
105
|
+
p.add_argument(
|
|
106
|
+
"--max-spend",
|
|
107
|
+
type=float,
|
|
108
|
+
default=None,
|
|
109
|
+
metavar="USD",
|
|
110
|
+
help="Refuse to synthesize if the estimated cost exceeds this USD cap.",
|
|
111
|
+
)
|
|
112
|
+
p.add_argument(
|
|
113
|
+
"--no-balance",
|
|
114
|
+
action="store_true",
|
|
115
|
+
help="Skip the upfront balance display.",
|
|
116
|
+
)
|
|
117
|
+
p.set_defaults(handler=_run)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---- input source resolution -------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def _read_input(args) -> Tuple[Optional[str], int]:
|
|
123
|
+
"""Resolve the input text. Returns (text, exit_code). text=None on error."""
|
|
124
|
+
sources = sum(
|
|
125
|
+
1 for v in (args.text and args.text != "-", args.from_file, args.stdin) if v
|
|
126
|
+
)
|
|
127
|
+
use_stdin = bool(args.stdin) or args.text == "-"
|
|
128
|
+
|
|
129
|
+
if args.from_file:
|
|
130
|
+
if args.text is not None and args.text != "-":
|
|
131
|
+
print("tts: cannot combine positional text with --from-file", file=sys.stderr)
|
|
132
|
+
return None, 2
|
|
133
|
+
try:
|
|
134
|
+
text = args.from_file.read_text(encoding="utf-8")
|
|
135
|
+
except OSError as e:
|
|
136
|
+
print(f"tts: cannot read {args.from_file}: {e}", file=sys.stderr)
|
|
137
|
+
return None, 2
|
|
138
|
+
elif use_stdin:
|
|
139
|
+
text = sys.stdin.read()
|
|
140
|
+
elif args.text:
|
|
141
|
+
text = args.text
|
|
142
|
+
else:
|
|
143
|
+
print(
|
|
144
|
+
"tts: input required (positional text, --from-file PATH, or --stdin)",
|
|
145
|
+
file=sys.stderr,
|
|
146
|
+
)
|
|
147
|
+
return None, 2
|
|
148
|
+
|
|
149
|
+
text = text.strip()
|
|
150
|
+
if not text:
|
|
151
|
+
print("tts: input is empty", file=sys.stderr)
|
|
152
|
+
return None, 2
|
|
153
|
+
return text, 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---- pricing + cost estimation ----------------------------------------------
|
|
157
|
+
|
|
158
|
+
def _fetch_tts_price_per_million(client, model: str) -> Optional[float]:
|
|
159
|
+
"""Look up `model_spec.pricing.input.usd` for the given TTS model. Best-effort."""
|
|
160
|
+
try:
|
|
161
|
+
doc = client.get_json("/models", params={"type": "tts"})
|
|
162
|
+
except VeniceAPIError:
|
|
163
|
+
return None
|
|
164
|
+
data = doc.get("data") if isinstance(doc, dict) else None
|
|
165
|
+
if not isinstance(data, list):
|
|
166
|
+
return None
|
|
167
|
+
for m in data:
|
|
168
|
+
if isinstance(m, dict) and m.get("id") == model:
|
|
169
|
+
try:
|
|
170
|
+
return float(m["model_spec"]["pricing"]["input"]["usd"])
|
|
171
|
+
except (KeyError, TypeError, ValueError):
|
|
172
|
+
return None
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _estimate_cost(char_count: int, price_per_million: Optional[float]) -> Optional[float]:
|
|
177
|
+
if price_per_million is None:
|
|
178
|
+
return None
|
|
179
|
+
return (char_count / 1_000_000.0) * price_per_million
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---- output path -------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def _short_id(text: str, model: str, voice: Optional[str]) -> str:
|
|
185
|
+
"""Stable 8-char hex tag derived from inputs; useful as a filename suffix."""
|
|
186
|
+
h = hashlib.sha1()
|
|
187
|
+
h.update(text.encode("utf-8"))
|
|
188
|
+
h.update(model.encode("utf-8"))
|
|
189
|
+
if voice:
|
|
190
|
+
h.update(voice.encode("utf-8"))
|
|
191
|
+
return h.hexdigest()[:8]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _resolve_output_path(arg_output: Optional[Path], short: str, fmt: str) -> Path:
|
|
195
|
+
ext = EXT_BY_FORMAT.get(fmt, ".bin")
|
|
196
|
+
default_name = f"venice-tts-{short}{ext}"
|
|
197
|
+
if arg_output is None:
|
|
198
|
+
return Path.cwd() / default_name
|
|
199
|
+
if arg_output.is_dir():
|
|
200
|
+
return arg_output / default_name
|
|
201
|
+
return arg_output
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---- main flow ---------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
def _print_estimate(cost: Optional[float], char_count: int, model: str) -> None:
|
|
207
|
+
if cost is None:
|
|
208
|
+
print(
|
|
209
|
+
f"Estimated cost: (unknown — could not fetch {model} pricing) "
|
|
210
|
+
f"[{char_count} chars]",
|
|
211
|
+
file=sys.stderr,
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
print(
|
|
215
|
+
f"Estimated cost: {billing.format_usd(cost)} "
|
|
216
|
+
f"({char_count} chars, model={model})",
|
|
217
|
+
file=sys.stderr,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _print_balance_and_remaining(client, cost: Optional[float], *, show: bool) -> None:
|
|
222
|
+
if not show:
|
|
223
|
+
return
|
|
224
|
+
info = None
|
|
225
|
+
try:
|
|
226
|
+
info = billing.fetch_balance(client)
|
|
227
|
+
except VeniceAPIError:
|
|
228
|
+
info = None
|
|
229
|
+
if not info or info.get("total") is None:
|
|
230
|
+
return
|
|
231
|
+
print(
|
|
232
|
+
f"Balance: {billing.format_balance_breakdown(info)}",
|
|
233
|
+
file=sys.stderr,
|
|
234
|
+
)
|
|
235
|
+
if cost is not None:
|
|
236
|
+
try:
|
|
237
|
+
remaining = float(info["total"]) - float(cost)
|
|
238
|
+
print(f"After charge: {billing.format_usd(remaining)}", file=sys.stderr)
|
|
239
|
+
except (TypeError, ValueError):
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _confirm_or_exit(yes: bool) -> Optional[int]:
|
|
244
|
+
if yes:
|
|
245
|
+
return None
|
|
246
|
+
if not sys.stdin.isatty():
|
|
247
|
+
print("non-interactive; pass --yes to confirm the charge.", file=sys.stderr)
|
|
248
|
+
return 1
|
|
249
|
+
try:
|
|
250
|
+
ans = input("Proceed? [y/N] ").strip().lower()
|
|
251
|
+
except EOFError:
|
|
252
|
+
ans = ""
|
|
253
|
+
if ans not in ("y", "yes"):
|
|
254
|
+
print("aborted by user", file=sys.stderr)
|
|
255
|
+
return 1
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _status_to_exit(err: VeniceAPIError) -> int:
|
|
260
|
+
s = err.status
|
|
261
|
+
if s == 422:
|
|
262
|
+
return 3
|
|
263
|
+
if s == 429:
|
|
264
|
+
return 4
|
|
265
|
+
if 500 <= s < 600:
|
|
266
|
+
return 5
|
|
267
|
+
if s == 404:
|
|
268
|
+
return 6
|
|
269
|
+
if s == 0:
|
|
270
|
+
return 8
|
|
271
|
+
return 2
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _validate_speed(speed: Optional[float]) -> Optional[int]:
|
|
275
|
+
if speed is None:
|
|
276
|
+
return None
|
|
277
|
+
if not (0.25 <= speed <= 4.0):
|
|
278
|
+
print(f"tts: --speed {speed} out of range (0.25-4.0)", file=sys.stderr)
|
|
279
|
+
return 2
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _run(args) -> int:
|
|
284
|
+
rc = _validate_speed(args.speed)
|
|
285
|
+
if rc is not None:
|
|
286
|
+
return rc
|
|
287
|
+
|
|
288
|
+
text, rc = _read_input(args)
|
|
289
|
+
if text is None:
|
|
290
|
+
return rc
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
client = build_client_from_auth()
|
|
294
|
+
except auth.AuthError as e:
|
|
295
|
+
print(str(e), file=sys.stderr)
|
|
296
|
+
return 2
|
|
297
|
+
|
|
298
|
+
price = _fetch_tts_price_per_million(client, args.model)
|
|
299
|
+
cost = _estimate_cost(len(text), price)
|
|
300
|
+
_print_estimate(cost, len(text), args.model)
|
|
301
|
+
_print_balance_and_remaining(client, cost, show=not args.no_balance)
|
|
302
|
+
|
|
303
|
+
if args.max_spend is not None and cost is not None:
|
|
304
|
+
try:
|
|
305
|
+
if float(cost) > float(args.max_spend):
|
|
306
|
+
print(
|
|
307
|
+
f"tts: estimate {billing.format_usd(cost)} exceeds "
|
|
308
|
+
f"--max-spend {billing.format_usd(args.max_spend)}; aborting",
|
|
309
|
+
file=sys.stderr,
|
|
310
|
+
)
|
|
311
|
+
return 1
|
|
312
|
+
except (TypeError, ValueError):
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
if args.dry_run:
|
|
316
|
+
return 0
|
|
317
|
+
|
|
318
|
+
rc = _confirm_or_exit(args.yes)
|
|
319
|
+
if rc is not None:
|
|
320
|
+
return rc
|
|
321
|
+
|
|
322
|
+
body: dict = {
|
|
323
|
+
"input": text,
|
|
324
|
+
"model": args.model,
|
|
325
|
+
"response_format": args.format,
|
|
326
|
+
}
|
|
327
|
+
if args.voice:
|
|
328
|
+
body["voice"] = args.voice
|
|
329
|
+
if args.speed is not None:
|
|
330
|
+
body["speed"] = args.speed
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
status, ctype, audio = client.request(
|
|
334
|
+
"POST", "/audio/speech", json_body=body
|
|
335
|
+
)
|
|
336
|
+
except VeniceAPIError as e:
|
|
337
|
+
print(f"tts failed: {e}", file=sys.stderr)
|
|
338
|
+
return _status_to_exit(e)
|
|
339
|
+
|
|
340
|
+
if not audio:
|
|
341
|
+
print("tts: server returned empty body", file=sys.stderr)
|
|
342
|
+
return 5
|
|
343
|
+
|
|
344
|
+
short = _short_id(text, args.model, args.voice)
|
|
345
|
+
out_path = _resolve_output_path(args.output, short, args.format)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
out_path.write_bytes(audio)
|
|
349
|
+
except OSError as e:
|
|
350
|
+
print(f"could not write {out_path}: {e}", file=sys.stderr)
|
|
351
|
+
return 9
|
|
352
|
+
|
|
353
|
+
abs_path = out_path.resolve()
|
|
354
|
+
print(str(abs_path))
|
|
355
|
+
print(f"wrote {len(audio)} bytes to {abs_path}", file=sys.stderr)
|
|
356
|
+
|
|
357
|
+
should_play = args.play
|
|
358
|
+
if should_play is None:
|
|
359
|
+
should_play = sys.stdout.isatty() and audio_player.has_player()
|
|
360
|
+
if should_play:
|
|
361
|
+
audio_player.play(out_path)
|
|
362
|
+
return 0
|