Skip to content

Commit 204d3ac

Browse files
committed
add cli 0.1.6
1 parent 0431be7 commit 204d3ac

File tree

5 files changed

+184
-11
lines changed

5 files changed

+184
-11
lines changed

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "veotools"
7-
version = "0.1.5"
7+
version = "0.1.6"
88
description = "A Python toolkit for AI-powered video generation and seamless stitching using Google's Veo models."
99
readme = "readme.md"
1010
requires-python = ">=3.10"
@@ -52,3 +52,6 @@ allow-direct-references = true
5252
[project.optional-dependencies]
5353
mcp = ["mcp[cli]>=1.0.0"]
5454

55+
[project.scripts]
56+
veo = "veotools.cli:main"
57+

readme.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ final = veo.stitch_videos(
6161
)
6262
```
6363

64+
## CLI
65+
66+
Install exposes the `veo` command. Use `-h/--help` on any subcommand.
67+
68+
```bash
69+
# Basics
70+
veo preflight
71+
veo list-models --remote
72+
73+
# Generate from text
74+
veo generate --prompt "cat riding a hat" --model veo-3.0-fast-generate-preview
75+
76+
# Continue a video and stitch seamlessly
77+
veo continue --video dog.mp4 --prompt "the dog finds a treasure chest" --overlap 1.0
78+
79+
# Help
80+
veo --help
81+
veo generate --help
82+
```
83+
6484
### Create a Story with Bridge
6585

6686
```python

veotools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
generate_cancel,
3636
)
3737

38-
__version__ = "0.1.5"
38+
__version__ = "0.1.6"
3939

4040
__all__ = [
4141
"VeoClient",

veotools/cli.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""Veotools command-line interface (no extra deps).
2+
3+
Usage examples:
4+
veo preflight
5+
veo list-models --remote
6+
veo generate --prompt "cat riding a hat" --model veo-3.0-fast-generate-preview
7+
veo continue --video dog.mp4 --prompt "the dog finds a treasure chest" --overlap 1.0
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import argparse
13+
import json
14+
from pathlib import Path
15+
from typing import Any, Dict, Optional
16+
17+
import veotools as veo
18+
19+
20+
def _print_progress(message: str, percent: int):
21+
bar_length = 24
22+
filled = int(bar_length * percent / 100)
23+
bar = "#" * filled + "-" * (bar_length - filled)
24+
print(f"[{bar}] {percent:3d}% {message}", end="\r")
25+
if percent >= 100:
26+
print()
27+
28+
29+
def cmd_preflight(_: argparse.Namespace) -> int:
30+
veo.init()
31+
data = veo.preflight()
32+
print(json.dumps(data, indent=2))
33+
return 0
34+
35+
36+
def cmd_list_models(ns: argparse.Namespace) -> int:
37+
veo.init()
38+
data = veo.list_models(include_remote=ns.remote)
39+
if ns.json:
40+
print(json.dumps(data, indent=2))
41+
else:
42+
for m in data.get("models", []):
43+
print(m.get("id"))
44+
return 0
45+
46+
47+
def cmd_generate(ns: argparse.Namespace) -> int:
48+
veo.init()
49+
kwargs: Dict[str, Any] = {}
50+
if ns.model:
51+
kwargs["model"] = ns.model
52+
if ns.image:
53+
result = veo.generate_from_image(
54+
image_path=Path(ns.image),
55+
prompt=ns.prompt,
56+
on_progress=_print_progress,
57+
)
58+
elif ns.video:
59+
result = veo.generate_from_video(
60+
video_path=Path(ns.video),
61+
prompt=ns.prompt,
62+
extract_at=ns.extract_at,
63+
on_progress=_print_progress,
64+
**kwargs,
65+
)
66+
else:
67+
result = veo.generate_from_text(
68+
ns.prompt,
69+
on_progress=_print_progress,
70+
**kwargs,
71+
)
72+
if ns.json:
73+
print(json.dumps(result.to_dict(), indent=2))
74+
else:
75+
print(result.path)
76+
return 0
77+
78+
79+
def cmd_continue(ns: argparse.Namespace) -> int:
80+
veo.init()
81+
# Generate continuation
82+
gen = veo.generate_from_video(
83+
video_path=Path(ns.video),
84+
prompt=ns.prompt,
85+
extract_at=ns.extract_at,
86+
model=ns.model,
87+
on_progress=_print_progress,
88+
)
89+
# Stitch with original
90+
stitched = veo.stitch_videos([Path(ns.video), Path(gen.path)], overlap=ns.overlap)
91+
if ns.json:
92+
out = {
93+
"generated": gen.to_dict(),
94+
"stitched": stitched.to_dict(),
95+
}
96+
print(json.dumps(out, indent=2))
97+
else:
98+
print(stitched.path)
99+
return 0
100+
101+
102+
def build_parser() -> argparse.ArgumentParser:
103+
p = argparse.ArgumentParser(prog="veo", description="Veotools CLI")
104+
sub = p.add_subparsers(dest="cmd", required=True)
105+
106+
s = sub.add_parser("preflight", help="Check environment and system prerequisites")
107+
s.set_defaults(func=cmd_preflight)
108+
109+
s = sub.add_parser("list-models", help="List available models")
110+
s.add_argument("--remote", action="store_true", help="Include remote discovery")
111+
s.add_argument("--json", action="store_true", help="Output JSON")
112+
s.set_defaults(func=cmd_list_models)
113+
114+
s = sub.add_parser("generate", help="Generate a video from text/image/video")
115+
s.add_argument("--prompt", required=True)
116+
s.add_argument("--model", help="Model ID (e.g., veo-3.0-fast-generate-preview)")
117+
s.add_argument("--image", help="Path to input image")
118+
s.add_argument("--video", help="Path to input video")
119+
s.add_argument("--extract-at", type=float, default=-1.0, help="Time offset for video continuation")
120+
s.add_argument("--json", action="store_true", help="Output JSON")
121+
s.set_defaults(func=cmd_generate)
122+
123+
s = sub.add_parser("continue", help="Continue a video and stitch seamlessly")
124+
s.add_argument("--video", required=True, help="Source video path")
125+
s.add_argument("--prompt", required=True)
126+
s.add_argument("--model", help="Model ID")
127+
s.add_argument("--extract-at", type=float, default=-1.0)
128+
s.add_argument("--overlap", type=float, default=1.0)
129+
s.add_argument("--json", action="store_true")
130+
s.set_defaults(func=cmd_continue)
131+
132+
return p
133+
134+
135+
def main(argv: Optional[list[str]] = None) -> int:
136+
parser = build_parser()
137+
ns = parser.parse_args(argv)
138+
return ns.func(ns)
139+
140+
141+
if __name__ == "__main__": # pragma: no cover
142+
raise SystemExit(main())
143+
144+

veotools/core.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def __init__(self, base_path: Optional[str] = None):
3434
3535
Default resolution order for base path:
3636
1. VEO_OUTPUT_DIR environment variable (if set)
37-
2. Project root alongside the package (../output)
38-
3. Current working directory (./output) as a safe fallback
37+
2. Current working directory (./output)
38+
3. Package-adjacent directory (../output) as a last resort
3939
"""
4040
resolved_base: Path
4141

@@ -46,15 +46,21 @@ def __init__(self, base_path: Optional[str] = None):
4646
elif env_base:
4747
resolved_base = Path(env_base)
4848
else:
49-
# 2) Attempt to place under project root (parent of the package dir)
49+
# 2) Prefer CWD/output for installed packages (CLI/scripts)
50+
cwd_candidate = Path.cwd() / "output"
5051
try:
51-
package_root = Path(__file__).resolve().parents[2]
52-
candidate = package_root / "output"
53-
candidate.mkdir(parents=True, exist_ok=True)
54-
resolved_base = candidate
52+
cwd_candidate.mkdir(parents=True, exist_ok=True)
53+
resolved_base = cwd_candidate
5554
except Exception:
56-
# 3) Fallback to CWD/output
57-
resolved_base = Path.cwd() / "output"
55+
# 3) As a last resort, place beside the installed package
56+
try:
57+
package_root = Path(__file__).resolve().parents[2]
58+
candidate = package_root / "output"
59+
candidate.mkdir(parents=True, exist_ok=True)
60+
resolved_base = candidate
61+
except Exception:
62+
# Final fallback: user home
63+
resolved_base = Path.home() / "output"
5864

5965
self.base_path = resolved_base
6066
self.base_path.mkdir(parents=True, exist_ok=True)

0 commit comments

Comments
 (0)