Adding support for handoff
This commit is contained in:
+52
-18
@@ -65,6 +65,16 @@ Examples:
|
||||
gnommo -p video1 up Push manifest files to rendering server
|
||||
gnommo -p video1 up --dry-run Preview which files would be pushed
|
||||
gnommo -p video1 down Pull files from rendering server to local
|
||||
gnommo -p video1 push Push project metadata to the local gnommoweb server
|
||||
gnommo -p video1 push --prod Push project metadata to production gnommoweb (glitch.university)
|
||||
gnommo -p video1 push --force Force push, overwriting the server copy
|
||||
gnommo -p video1 pull Pull (fetch) project metadata from the local server
|
||||
gnommo -p video1 pull --prod Pull project metadata from production
|
||||
gnommo -p video1 pull --force Force pull, overwriting the local copy
|
||||
gnommo -p video1 handoff --prod Upload the rendered video for online review (glitch.university/review/<id>)
|
||||
gnommo -p video1 handoff Upload the rendered video to the local server
|
||||
gnommo -p video1 handoff --file X Upload a specific video file instead of out/<output_video>
|
||||
Note: 'push' sends metadata (script/slides/etc); 'handoff' uploads the actual video file.
|
||||
gnommo -p video1 extract-audio --combined Extract audio from narration_combined.mov
|
||||
gnommo -p video1 extract-audio --combined --channel left Extract left channel only
|
||||
gnommo -p video1 extract-audio --segment seg01 Extract from a specific segment
|
||||
@@ -4131,21 +4141,34 @@ def cmd_render(
|
||||
audio, audio_dir = parse_audio(project_path, config)
|
||||
|
||||
# Load whisper transcription JSON
|
||||
# Check for narration_combined in videos.json (new workflow) or multi-segment in config (legacy)
|
||||
# Resolve the combined narration skeleton. The .mov file — not the videos.json
|
||||
# entry — is the source of truth: stitch run without the external drive leaves
|
||||
# the file in media/videos/ even when the videos.json entry is absent (e.g. a
|
||||
# metadata pull overwrote it). Legacy multi-segment projects are handled below.
|
||||
combined_path = videos_dir / "narration_combined.mov"
|
||||
resolved_combined = _resolve_narration_combined(project_path, videos_dir, config)
|
||||
if resolved_combined and resolved_combined != combined_path:
|
||||
# File lives on external disk — point the VideoSource at the absolute path so
|
||||
# the renderer doesn't re-resolve it via the local (missing) videos_dir.
|
||||
if "narration_combined" in videos:
|
||||
narration_json = project_path / "media" / "narration" / "narration.json"
|
||||
_narr_segments = _read_json(narration_json) if narration_json.exists() else {}
|
||||
if resolved_combined and resolved_combined.exists():
|
||||
# File is available (locally or via process cache). Ensure a videos.json
|
||||
# entry exists — synthesizing one when stitch's entry was lost — then use it.
|
||||
if "narration_combined" not in videos:
|
||||
from .models import VideoSource
|
||||
|
||||
_first_seg = next(iter(_narr_segments.values()), {})
|
||||
_seg_cutout = (
|
||||
_first_seg.get("cutout") if isinstance(_first_seg, dict) else None
|
||||
)
|
||||
videos["narration_combined"] = VideoSource(
|
||||
source_file="narration_combined.mov",
|
||||
cutout=_seg_cutout or "talkinghead",
|
||||
always_visible=True,
|
||||
volume=1.0,
|
||||
)
|
||||
if resolved_combined != combined_path:
|
||||
# File lives on external disk — point the VideoSource at the absolute
|
||||
# path so the renderer doesn't re-resolve it via the local videos_dir.
|
||||
videos["narration_combined"].source_file = str(resolved_combined)
|
||||
if (
|
||||
"narration_combined" in videos
|
||||
and resolved_combined
|
||||
and resolved_combined.exists()
|
||||
):
|
||||
# New workflow: narration_combined was created by 'gnommo concat' and is in videos.json
|
||||
# This entry has the correct volume setting from videos.json
|
||||
transcript_path = resolved_combined.with_suffix(".transcript.json")
|
||||
config.main_video = "narration_combined"
|
||||
if verbose:
|
||||
@@ -4188,10 +4211,21 @@ def cmd_render(
|
||||
|
||||
if verbose:
|
||||
print(f" Using combined narration: {combined_path.name}")
|
||||
else:
|
||||
# Check if narration.json exists with segments (new workflow) - if so, require narration_combined
|
||||
narration_json = project_path / "media" / "narration" / "narration.json"
|
||||
if narration_json.exists() and _read_json(narration_json):
|
||||
elif _narr_segments:
|
||||
# narration.json has segments, but the combined .mov could not be found.
|
||||
# Distinguish "stitched, file unreachable" from "never stitched" so the
|
||||
# hint is actionable instead of always blaming videos.json.
|
||||
if "narration_combined" in videos:
|
||||
print(
|
||||
f"Error: narration_combined.mov could not be found.", file=sys.stderr
|
||||
)
|
||||
print(
|
||||
f"videos.json references narration_combined, but the file is not on disk "
|
||||
f"(checked local media/videos and the process cache).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(_narration_combined_hint(project_path, config), file=sys.stderr)
|
||||
else:
|
||||
print(
|
||||
f"Error: narration_combined not found in videos.json", file=sys.stderr
|
||||
)
|
||||
@@ -4203,8 +4237,8 @@ def cmd_render(
|
||||
f"Run 'gnommo -p {project_path.name} stitch' first.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
return 1
|
||||
else:
|
||||
# Single video - look for .transcript.json next to the narration video
|
||||
result = _find_narration_video(config, videos)
|
||||
if result:
|
||||
|
||||
+54
-74
@@ -1,27 +1,27 @@
|
||||
"""Hand off a finished video to MinIO storage via gnommoeditor (prod) or gnommoweb (local).
|
||||
"""Hand off a finished video to gnommoweb (the review app) — MinIO upload + version bump.
|
||||
|
||||
Works for any gnommo project type: parent videos and shorts alike.
|
||||
|
||||
Usage:
|
||||
gnommo handoff -p video1
|
||||
gnommo handoff -p short_pixelated_universe
|
||||
gnommo handoff -p video1 # → local gnommoweb
|
||||
gnommo handoff -p video1 --prod # → production gnommoweb (glitch.university)
|
||||
gnommo handoff -p video1 --file /path/to/render.mp4
|
||||
|
||||
Reads project.json for the 'output_video' field (path relative to the
|
||||
project directory). Override with --file.
|
||||
|
||||
On success (production):
|
||||
- Uploads the video to MinIO via POST /api/assets/upload on gnommoeditor
|
||||
- Updates .gnommo_sync.prod.json with asset URL
|
||||
|
||||
On success (local):
|
||||
- Uploads via POST /api/projects/:handle/handoff on gnommoweb
|
||||
- Updates .gnommo_sync.json with new video_version
|
||||
On success (both local and --prod):
|
||||
- Uploads the video via POST /api/projects/:id/handoff on gnommoweb, which
|
||||
stores it in MinIO and bumps the project's video_version (so it shows up on
|
||||
the review page).
|
||||
- Updates .gnommo_sync.json (local) / .gnommo_sync.prod.json (--prod) with the
|
||||
new video_version.
|
||||
|
||||
Configuration (from .env or environment):
|
||||
GNOMMOEDITOR_URL Base URL for production (e.g. https://editor.glitch.university)
|
||||
GNOMMOWEB_URL Base URL for local dev (e.g. http://localhost:3001)
|
||||
GNOMMOWEB_API_KEY Bearer token (CONTENT_API_KEY from gnommoweb)
|
||||
GNOMMOWEB_URL Base URL for local dev (e.g. http://localhost:3001)
|
||||
GNOMMOWEB_API_KEY Bearer token for local (CONTENT_API_KEY from gnommoweb)
|
||||
GNOMMOWEB_PROD_URL Base URL for production (e.g. https://glitch.university)
|
||||
GNOMMOWEB_PROD_API_KEY Bearer token for production (CONTENT_API_KEY)
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -85,24 +85,26 @@ def cmd_handoff(
|
||||
) -> int:
|
||||
_load_env_file()
|
||||
|
||||
# Handoff always targets gnommoweb (the review app). --prod selects the
|
||||
# production instance; without it, the local dev server.
|
||||
if prod:
|
||||
api_url = os.environ.get("GNOMMOEDITOR_URL", "").rstrip("/")
|
||||
if not api_url:
|
||||
print("Error: GNOMMOEDITOR_URL is not set.", file=sys.stderr)
|
||||
return 1
|
||||
api_url = os.environ.get("GNOMMOWEB_PROD_URL", "").rstrip("/")
|
||||
api_key = os.environ.get("GNOMMOWEB_PROD_API_KEY", "")
|
||||
url_var, key_var = "GNOMMOWEB_PROD_URL", "GNOMMOWEB_PROD_API_KEY"
|
||||
else:
|
||||
api_url = os.environ.get("GNOMMOWEB_URL", "").rstrip("/")
|
||||
api_key = os.environ.get("GNOMMOWEB_API_KEY", "")
|
||||
if not api_url:
|
||||
print("Error: GNOMMOWEB_URL is not set.", file=sys.stderr)
|
||||
return 1
|
||||
if not api_key:
|
||||
print("Error: GNOMMOWEB_API_KEY is not set.", file=sys.stderr)
|
||||
return 1
|
||||
url_var, key_var = "GNOMMOWEB_URL", "GNOMMOWEB_API_KEY"
|
||||
if not api_url:
|
||||
print(f"Error: {url_var} is not set.", file=sys.stderr)
|
||||
return 1
|
||||
if not api_key:
|
||||
print(f"Error: {key_var} is not set.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if verbose:
|
||||
target = "production (gnommoeditor)" if prod else "local"
|
||||
print(f" → {target}: {api_url}")
|
||||
target = "production" if prod else "local"
|
||||
print(f" → gnommoweb {target}: {api_url}")
|
||||
|
||||
project_file = project_path / "project.json"
|
||||
if not project_file.exists():
|
||||
@@ -145,24 +147,16 @@ def cmd_handoff(
|
||||
print(f" File: {video_path} ({file_size_mb:.1f} MB)")
|
||||
|
||||
# ── Upload ─────────────────────────────────────────────────────────────────
|
||||
# gnommoweb: POST /api/projects/:id/handoff — uploads to MinIO and bumps the
|
||||
# project's video_version so it appears on the review page.
|
||||
try:
|
||||
if prod:
|
||||
# gnommoeditor: POST /api/assets/upload — field name is 'file', no auth
|
||||
with open(video_path, "rb") as vf:
|
||||
r = requests.post(
|
||||
f"{api_url}/api/assets/upload",
|
||||
files={"file": (video_path.name, vf, _mime_type(video_path))},
|
||||
timeout=None,
|
||||
)
|
||||
else:
|
||||
# gnommoweb: POST /api/projects/:id/handoff
|
||||
with open(video_path, "rb") as vf:
|
||||
r = requests.post(
|
||||
f"{api_url}/api/projects/{project_id}/handoff",
|
||||
files={"video": (video_path.name, vf, _mime_type(video_path))},
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=None,
|
||||
)
|
||||
with open(video_path, "rb") as vf:
|
||||
r = requests.post(
|
||||
f"{api_url}/api/projects/{project_id}/handoff",
|
||||
files={"video": (video_path.name, vf, _mime_type(video_path))},
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=None,
|
||||
)
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"✗ Could not connect to {api_url}")
|
||||
return 1
|
||||
@@ -178,40 +172,26 @@ def cmd_handoff(
|
||||
result = r.json()
|
||||
|
||||
# ── Write sync state ───────────────────────────────────────────────────────
|
||||
# gnommoweb response: { video_version, video_url, asset: { updated_at } }
|
||||
now_iso = datetime.now(tz=timezone.utc).isoformat(timespec="seconds")
|
||||
existing_sync = _read_sync(project_path, prod)
|
||||
|
||||
if prod:
|
||||
# gnommoeditor response: { asset: { id, url, minio_object_key, ... } }
|
||||
asset = result.get("asset", {})
|
||||
asset_url = asset.get("url", "")
|
||||
_write_sync(
|
||||
project_path,
|
||||
{**existing_sync, "last_handoff_at": now_iso, "asset_url": asset_url},
|
||||
prod,
|
||||
)
|
||||
print(f"✓ {project_id} → uploaded [asset #{asset.get('id')}]")
|
||||
if asset_url:
|
||||
print(f" {asset_url}")
|
||||
else:
|
||||
# gnommoweb response: { video_version, video_url, asset: { updated_at } }
|
||||
video_version = result.get("video_version", "?")
|
||||
video_url = result.get("video_url", "")
|
||||
_write_sync(
|
||||
project_path,
|
||||
{
|
||||
**existing_sync,
|
||||
"last_handoff_at": now_iso,
|
||||
"video_version": video_version,
|
||||
"server_updated_at": result.get("asset", {}).get(
|
||||
"updated_at", existing_sync.get("server_updated_at")
|
||||
),
|
||||
},
|
||||
prod,
|
||||
)
|
||||
print(f"✓ {project_id} → v{video_version} [processed]")
|
||||
if video_url:
|
||||
print(f" {video_url}")
|
||||
video_version = result.get("video_version", "?")
|
||||
video_url = result.get("video_url", "")
|
||||
_write_sync(
|
||||
project_path,
|
||||
{
|
||||
**existing_sync,
|
||||
"last_handoff_at": now_iso,
|
||||
"video_version": video_version,
|
||||
"server_updated_at": result.get("asset", {}).get(
|
||||
"updated_at", existing_sync.get("server_updated_at")
|
||||
),
|
||||
},
|
||||
prod,
|
||||
)
|
||||
print(f"✓ {project_id} → v{video_version} [uploaded]")
|
||||
if video_url:
|
||||
print(f" {video_url}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
+30
-207
@@ -1,19 +1,18 @@
|
||||
"""Push project metadata to gnommoeditor (prod) or gnommoweb (local).
|
||||
"""Push project metadata to gnommoweb (the review app).
|
||||
|
||||
Usage:
|
||||
gnommo push -p video1 # push parent video project
|
||||
gnommo push -p video1 # push to local gnommoweb
|
||||
gnommo push -p video1 --prod # push to production gnommoweb
|
||||
gnommo push -p short_pixelated_universe # push a short project
|
||||
gnommo push -p myproject --force # force push, overwrite server
|
||||
|
||||
Reads project.json and companion JSON files, then POSTs to:
|
||||
Production: POST /api/ingest (gnommoeditor, uses INGEST_API_KEY)
|
||||
Local: POST /api/projects/push (gnommoweb, uses GNOMMOWEB_API_KEY)
|
||||
POSTs the project metadata to gnommoweb's POST /api/projects/push.
|
||||
|
||||
Configuration (from .env or environment):
|
||||
GNOMMOEDITOR_URL Base URL for production (e.g. https://editor.glitch.university)
|
||||
INGEST_API_KEY Bearer token for gnommoeditor ingest endpoint
|
||||
GNOMMOWEB_URL Base URL for local dev (e.g. http://localhost:3001)
|
||||
GNOMMOWEB_API_KEY Bearer token for local gnommoweb
|
||||
GNOMMOWEB_URL Base URL for local dev (e.g. http://localhost:3001)
|
||||
GNOMMOWEB_API_KEY Bearer token for local (CONTENT_API_KEY)
|
||||
GNOMMOWEB_PROD_URL Base URL for production (e.g. https://glitch.university)
|
||||
GNOMMOWEB_PROD_API_KEY Bearer token for production (CONTENT_API_KEY)
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -68,130 +67,6 @@ def _write_sync(project_path: Path, data: dict, prod: bool = False):
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
def _load_json_file(path: Path, label: str, verbose: bool) -> dict | list | None:
|
||||
"""Load a JSON file, returning None if it doesn't exist."""
|
||||
if not path.exists():
|
||||
if verbose:
|
||||
print(f" {label}: not found at {path}")
|
||||
return None
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f" Warning: could not parse {label} ({path}): {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def _load_text_file(path: Path, label: str) -> str | None:
|
||||
"""Load a text file, returning None if it doesn't exist."""
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
return path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return path.read_text(encoding="latin-1")
|
||||
|
||||
|
||||
def _parse_seconds(value) -> float | None:
|
||||
"""Convert a time value like '30s', '1:30', or 30 into a plain float of seconds."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
value = str(value).strip()
|
||||
if value.endswith("s"):
|
||||
value = value[:-1]
|
||||
if ":" in value:
|
||||
parts = value.split(":")
|
||||
if len(parts) == 2:
|
||||
return float(parts[0]) * 60 + float(parts[1])
|
||||
elif len(parts) == 3:
|
||||
return float(parts[0]) * 3600 + float(parts[1]) * 60 + float(parts[2])
|
||||
return float(value)
|
||||
|
||||
|
||||
def _sanitize_time_fields(data: dict | None, fields: list[str]) -> dict | None:
|
||||
"""Return a copy of dict with the given fields converted to plain floats."""
|
||||
if not data:
|
||||
return data
|
||||
result = dict(data)
|
||||
for field in fields:
|
||||
if field in result and result[field] is not None:
|
||||
try:
|
||||
result[field] = _parse_seconds(result[field])
|
||||
except (ValueError, TypeError):
|
||||
pass # leave invalid values for the server to reject with a clear error
|
||||
return result
|
||||
|
||||
|
||||
def _build_ingest_payload(project: dict, project_path: Path, verbose: bool) -> dict:
|
||||
"""Build the rich ingest payload for gnommoeditor POST /api/ingest."""
|
||||
|
||||
# ── slides ────────────────────────────────────────────────────────────────
|
||||
slides_path_str = project.get("slides", "slides.json")
|
||||
slides_path = project_path / slides_path_str
|
||||
slides = _load_json_file(slides_path, "slides", verbose)
|
||||
if slides and verbose:
|
||||
print(f" slides: {len(slides)} entries")
|
||||
|
||||
# ── manuscript ────────────────────────────────────────────────────────────
|
||||
manuscript_path_str = project.get("manuscript", "manuscript.txt")
|
||||
manuscript_path = project_path / manuscript_path_str
|
||||
manuscript = _load_text_file(manuscript_path, "manuscript")
|
||||
if manuscript:
|
||||
print(f" manuscript: {len(manuscript)} chars")
|
||||
elif verbose:
|
||||
print(f" manuscript: not found at {manuscript_path}")
|
||||
|
||||
# ── narration ─────────────────────────────────────────────────────────────
|
||||
narration_path_str = project.get("narration", "narration.json")
|
||||
narration_path = project_path / narration_path_str
|
||||
narration = _load_json_file(narration_path, "narration", verbose)
|
||||
|
||||
# ── audio ─────────────────────────────────────────────────────────────────
|
||||
audio_path_str = project.get("audio_tracks", "audio.json")
|
||||
audio_path = project_path / audio_path_str
|
||||
audio = _load_json_file(audio_path, "audio", verbose)
|
||||
|
||||
# ── videos ────────────────────────────────────────────────────────────────
|
||||
videos_path_str = project.get("videos", "videos.json")
|
||||
videos_path = project_path / videos_path_str
|
||||
videos = _load_json_file(videos_path, "videos", verbose)
|
||||
|
||||
# ── citations ─────────────────────────────────────────────────────────────
|
||||
citations_path = project_path / "citations.json"
|
||||
citations = _load_json_file(citations_path, "citations", verbose)
|
||||
|
||||
# Sanitize time fields — convert "30s", "1:30" etc. to plain floats
|
||||
_VIDEO_TIME_FIELDS = ["duration", "pause_narration", "skip", "take"]
|
||||
_NARRATION_TIME_FIELDS = ["skip", "take"]
|
||||
_AUDIO_TIME_FIELDS = ["overlap", "duration"]
|
||||
|
||||
if videos:
|
||||
videos = {
|
||||
k: _sanitize_time_fields(v, _VIDEO_TIME_FIELDS) for k, v in videos.items()
|
||||
}
|
||||
if narration:
|
||||
narration = {
|
||||
k: _sanitize_time_fields(v, _NARRATION_TIME_FIELDS)
|
||||
for k, v in narration.items()
|
||||
}
|
||||
if audio:
|
||||
audio = {
|
||||
k: _sanitize_time_fields(v, _AUDIO_TIME_FIELDS) for k, v in audio.items()
|
||||
}
|
||||
|
||||
return {
|
||||
"project": project,
|
||||
"slides": slides,
|
||||
"manuscript": manuscript,
|
||||
"narration": narration,
|
||||
"audio": audio,
|
||||
"videos": videos,
|
||||
"citations": citations,
|
||||
}
|
||||
|
||||
|
||||
def cmd_push(
|
||||
project_path: Path, verbose: bool = False, force: bool = False, prod: bool = False
|
||||
) -> int:
|
||||
@@ -211,86 +86,34 @@ def cmd_push(
|
||||
print("Error: project.json must have 'id' and 'name' fields.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return _push_gnommoweb(project, project_path, verbose, force, prod)
|
||||
|
||||
|
||||
# ── gnommoweb POST /api/projects/push ─────────────────────────────────────────
|
||||
|
||||
|
||||
def _push_gnommoweb(
|
||||
project: dict, project_path: Path, verbose: bool, force: bool, prod: bool
|
||||
) -> int:
|
||||
# --prod selects the production gnommoweb instance; without it, local dev.
|
||||
if prod:
|
||||
return _push_prod(project, project_path, verbose)
|
||||
api_url = os.environ.get("GNOMMOWEB_PROD_URL", "").rstrip("/")
|
||||
api_key = os.environ.get("GNOMMOWEB_PROD_API_KEY", "")
|
||||
url_var, key_var = "GNOMMOWEB_PROD_URL", "GNOMMOWEB_PROD_API_KEY"
|
||||
else:
|
||||
return _push_local(project, project_path, verbose, force)
|
||||
|
||||
|
||||
# ── Production: gnommoeditor POST /api/ingest ─────────────────────────────────
|
||||
|
||||
|
||||
def _push_prod(project: dict, project_path: Path, verbose: bool) -> int:
|
||||
api_url = os.environ.get("GNOMMOEDITOR_URL", "").rstrip("/")
|
||||
api_key = os.environ.get("INGEST_API_KEY", "")
|
||||
api_url = os.environ.get("GNOMMOWEB_URL", "").rstrip("/")
|
||||
api_key = os.environ.get("GNOMMOWEB_API_KEY", "")
|
||||
url_var, key_var = "GNOMMOWEB_URL", "GNOMMOWEB_API_KEY"
|
||||
if not api_url:
|
||||
print("Error: GNOMMOEDITOR_URL is not set.", file=sys.stderr)
|
||||
print(f"Error: {url_var} is not set.", file=sys.stderr)
|
||||
return 1
|
||||
if not api_key:
|
||||
print("Error: INGEST_API_KEY is not set.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
project_id = project["id"]
|
||||
payload = _build_ingest_payload(project, project_path, verbose)
|
||||
|
||||
# Attach sync state so the server can record it
|
||||
sync = _read_sync(project_path, prod=True)
|
||||
if sync:
|
||||
payload["sync"] = sync
|
||||
|
||||
print(f" → {api_url}/api/ingest")
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{api_url}/api/ingest",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=30,
|
||||
)
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"✗ Could not connect to {api_url}")
|
||||
return 1
|
||||
|
||||
if not r.ok:
|
||||
try:
|
||||
body = r.json()
|
||||
except Exception:
|
||||
body = r.text[:500]
|
||||
print(f"✗ Server returned {r.status_code}: {body}")
|
||||
return 1
|
||||
|
||||
result = r.json()
|
||||
video_id = result.get("video_id")
|
||||
slides_upserted = result.get("slides_upserted", 0)
|
||||
|
||||
# Update sync state
|
||||
now_iso = datetime.now(tz=timezone.utc).isoformat(timespec="seconds")
|
||||
existing_sync = _read_sync(project_path, prod=True)
|
||||
_write_sync(
|
||||
project_path,
|
||||
{**existing_sync, "last_pushed_at": now_iso},
|
||||
prod=True,
|
||||
)
|
||||
|
||||
print(f"✓ {project_id} → video #{video_id} ({slides_upserted} slides)")
|
||||
return 0
|
||||
|
||||
|
||||
# ── Local dev: gnommoweb POST /api/projects/push ──────────────────────────────
|
||||
|
||||
|
||||
def _push_local(project: dict, project_path: Path, verbose: bool, force: bool) -> int:
|
||||
api_url = os.environ.get("GNOMMOWEB_URL", "").rstrip("/")
|
||||
api_key = os.environ.get("GNOMMOWEB_API_KEY", "")
|
||||
if not api_url:
|
||||
print("Error: GNOMMOWEB_URL is not set.", file=sys.stderr)
|
||||
return 1
|
||||
if not api_key:
|
||||
print("Error: GNOMMOWEB_API_KEY is not set.", file=sys.stderr)
|
||||
print(f"Error: {key_var} is not set.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if verbose:
|
||||
print(f" → local: {api_url}")
|
||||
target = "production" if prod else "local"
|
||||
print(f" → gnommoweb {target}: {api_url}")
|
||||
|
||||
project_id = project["id"]
|
||||
parent_project = project.get("parent_project")
|
||||
@@ -327,7 +150,7 @@ def _push_local(project: dict, project_path: Path, verbose: bool, force: bool) -
|
||||
server_updated_at = result.get("server_updated_at")
|
||||
|
||||
now_iso = datetime.now(tz=timezone.utc).isoformat(timespec="seconds")
|
||||
existing_sync = _read_sync(project_path, prod=False)
|
||||
existing_sync = _read_sync(project_path, prod=prod)
|
||||
_write_sync(
|
||||
project_path,
|
||||
{
|
||||
@@ -335,7 +158,7 @@ def _push_local(project: dict, project_path: Path, verbose: bool, force: bool) -
|
||||
"last_pushed_at": now_iso,
|
||||
"server_updated_at": server_updated_at,
|
||||
},
|
||||
prod=False,
|
||||
prod=prod,
|
||||
)
|
||||
|
||||
asset = result.get("asset", {})
|
||||
|
||||
Reference in New Issue
Block a user