Adding push and pull commands
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
[S1]
|
||||
What if the universe isn’t continuous?
|
||||
What if it only looks smooth… because we’ve never zoomed in the right way?
|
||||
|
||||
[S2]
|
||||
At Glitch University, our first public course asks a strange question:
|
||||
Is the universe fundamentally pixelated?
|
||||
Blocky?
|
||||
Like Minecraft - just with absurdly tiny blocks?
|
||||
|
||||
[S3]
|
||||
This question has been around forever.
|
||||
But it's always been filed under "too weird to bother."
|
||||
That's about to change.
|
||||
|
||||
[S4]
|
||||
Explore the tech-tree.
|
||||
Level up.
|
||||
Run experiments on real data from space.
|
||||
We're committed to scientific rigour.
|
||||
Falsifiability. Truth-seeking.
|
||||
And not being a complete bore.
|
||||
|
||||
|
||||
[S5]
|
||||
Don’t enroll now.
|
||||
Enroll later.
|
||||
|
||||
[S6]
|
||||
Glitch University
|
||||
Later is now.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"S1": {
|
||||
"image": "GlitchTrailer.001.png",
|
||||
"type": "fullscreen"
|
||||
},
|
||||
"S2": {
|
||||
"image": "GlitchTrailer.002.png",
|
||||
"type": "fullscreen"
|
||||
},
|
||||
"S3": {
|
||||
"image": "GlitchTrailer.003.png",
|
||||
"type": "fullscreen"
|
||||
},
|
||||
"S4": {
|
||||
"image": "GlitchTrailer.004.png",
|
||||
"type": "fullscreen"
|
||||
},
|
||||
"S5": {
|
||||
"image": "GlitchTrailer.005.png",
|
||||
"type": "fullscreen"
|
||||
},
|
||||
"S6": {
|
||||
"image": "GlitchTrailer.006.png",
|
||||
"type": "fullscreen"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"id": "GlitchTrailer",
|
||||
"coursecode": "TRAILER",
|
||||
"name": "Glitch University trailer",
|
||||
"description": "Welcome to Glitch University.",
|
||||
"hook": null,
|
||||
"platform_targets": ["youtube"],
|
||||
"status": "scripted",
|
||||
"youtube_url": null,
|
||||
"resolution": [1960, 1080],
|
||||
"fps": 30,
|
||||
"duration_seconds": null,
|
||||
"default_filters": {
|
||||
"audioonly": [
|
||||
{
|
||||
"type": "audio_normalize",
|
||||
"normalize": true,
|
||||
"target_lufs": -14,
|
||||
"target_lra": 11,
|
||||
"target_tp": -1.5
|
||||
}
|
||||
],
|
||||
"talkinghead": [
|
||||
{
|
||||
"type": "audio_normalize",
|
||||
"highpass": 100,
|
||||
"room_eq": true,
|
||||
"room_eq_freq": 300,
|
||||
"room_eq_gain": -4,
|
||||
"room_eq_width": 1.5,
|
||||
"dereverb_model": "shared_assets/models/std.rnnn",
|
||||
"dereverb_mix": 0.8,
|
||||
"denoise": true,
|
||||
"noise_floor": -25,
|
||||
"gate": true,
|
||||
"gate_threshold": -35,
|
||||
"gate_range": -20,
|
||||
"compress": true,
|
||||
"threshold": -20,
|
||||
"ratio": 4,
|
||||
"attack": 5,
|
||||
"release": 50,
|
||||
"makeup": 2,
|
||||
"normalize": true,
|
||||
"target_lufs": -16,
|
||||
"target_lra": 11,
|
||||
"target_tp": -1.5
|
||||
},
|
||||
{
|
||||
"type": "color_grade",
|
||||
"saturation": 1.15,
|
||||
"contrast": 1.05,
|
||||
"bm": -0.1,
|
||||
"rm": 0.04
|
||||
},
|
||||
{
|
||||
"type": "gnommokey",
|
||||
"screen_color": [
|
||||
81,
|
||||
137,
|
||||
65
|
||||
],
|
||||
"screen_gain": 175,
|
||||
"screen_balance": 58,
|
||||
"despill_bias": [
|
||||
217,
|
||||
240,
|
||||
255
|
||||
],
|
||||
"despill_strength": 5.0,
|
||||
"edge_erode": 1.0,
|
||||
"clip_black": 0,
|
||||
"clip_white": 100
|
||||
},
|
||||
{
|
||||
"type": "mask",
|
||||
"left": 0.05,
|
||||
"right": 0.1,
|
||||
"top": 0.1,
|
||||
"bottom": 0.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"cutouts": {
|
||||
"talkinghead": {
|
||||
"x": "-23%",
|
||||
"y": "10%",
|
||||
"height": "90%"
|
||||
},
|
||||
"square": {
|
||||
"x": "46.5%",
|
||||
"y": "4.5%",
|
||||
"width": "50%",
|
||||
"height": "90%"
|
||||
},
|
||||
"fullscreen": {
|
||||
"x": "0%",
|
||||
"y": "0%",
|
||||
"height": "100%"
|
||||
}
|
||||
},
|
||||
"manuscript": "manuscript.txt",
|
||||
"shorts": [],
|
||||
"output_video": "out/final.mp4"
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"defaultSlideType": "fullscreen",
|
||||
"keynote_file": "media/example.key",
|
||||
"transcript": "media/videos/talking_head.transcript.json",
|
||||
"narration": "media/narration/narration.json",
|
||||
"background": "shared_assets/solarpunk.png",
|
||||
"videos": "media/videos/videos.json",
|
||||
"slides": "media/slides/Example/slides.json",
|
||||
@@ -71,7 +72,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"main_video": ["talking_head_S1", "talking_head_S3"],
|
||||
"cutouts": {
|
||||
"talkinghead": {
|
||||
"x": "-10%",
|
||||
|
||||
+17
-13
@@ -33,7 +33,11 @@ except ImportError:
|
||||
print("Error: 'requests' package is required. Run: pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
SYNC_FILE = ".gnommo_sync.json"
|
||||
SYNC_FILE_LOCAL = ".gnommo_sync.json"
|
||||
SYNC_FILE_PROD = ".gnommo_sync.prod.json"
|
||||
|
||||
def _sync_file(prod: bool) -> str:
|
||||
return SYNC_FILE_PROD if prod else SYNC_FILE_LOCAL
|
||||
|
||||
|
||||
def _load_env_file():
|
||||
@@ -52,16 +56,16 @@ def _load_env_file():
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def _read_sync(project_path: Path) -> dict:
|
||||
sync_file = project_path / SYNC_FILE
|
||||
def _read_sync(project_path: Path, prod: bool = False) -> dict:
|
||||
sync_file = project_path / _sync_file(prod)
|
||||
if sync_file.exists():
|
||||
with open(sync_file) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def _write_sync(project_path: Path, data: dict):
|
||||
with open(project_path / SYNC_FILE, "w") as f:
|
||||
def _write_sync(project_path: Path, data: dict, prod: bool = False):
|
||||
with open(project_path / _sync_file(prod), "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
@@ -127,16 +131,16 @@ def cmd_handoff(project_path: Path, verbose: bool = False, file_override: str |
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=None, # large files may take a while
|
||||
)
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"Error: Could not connect to {api_url}", file=sys.stderr)
|
||||
print(f"✗ Could not connect to {api_url}")
|
||||
return 1
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"Error: Server returned {e.response.status_code}", file=sys.stderr)
|
||||
|
||||
if not r.ok:
|
||||
try:
|
||||
print(f" {e.response.json()}", file=sys.stderr)
|
||||
body = r.json()
|
||||
except Exception:
|
||||
pass
|
||||
body = r.text[:500]
|
||||
print(f"✗ Server returned {r.status_code}: {body}")
|
||||
return 1
|
||||
|
||||
result = r.json()
|
||||
@@ -145,13 +149,13 @@ def cmd_handoff(project_path: Path, verbose: bool = False, file_override: str |
|
||||
|
||||
# ── Write sync state ───────────────────────────────────────────────────────
|
||||
now_iso = datetime.now(tz=timezone.utc).isoformat(timespec="seconds")
|
||||
existing_sync = _read_sync(project_path)
|
||||
existing_sync = _read_sync(project_path, prod)
|
||||
_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:
|
||||
|
||||
@@ -1025,6 +1025,38 @@ def apply_combined_video_filters_chunked(
|
||||
f" Completed chunk {i+1}/{len(chunk_tasks)} ({completed}/{len(chunk_tasks)} done)"
|
||||
)
|
||||
|
||||
# Concatenate chunks into final output
|
||||
concat_list = scratch_dir / "concat.txt"
|
||||
with open(concat_list, "w") as cf:
|
||||
for chunk_path in chunk_files:
|
||||
cf.write(f"file '{chunk_path.resolve()}'\n")
|
||||
|
||||
if verbose:
|
||||
print(f" Concatenating {len(chunk_files)} chunks → {output_path.name}")
|
||||
|
||||
concat_cmd = [
|
||||
"ffmpeg", "-y",
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
"-i", str(concat_list),
|
||||
"-c", "copy",
|
||||
str(output_path),
|
||||
]
|
||||
concat_result = run_ffmpeg_with_progress(concat_cmd, duration, "Concatenating")
|
||||
if concat_result.returncode != 0:
|
||||
raise PreprocessError(
|
||||
"Chunk concatenation failed",
|
||||
filter_type="concat",
|
||||
command=" ".join(concat_cmd),
|
||||
stderr=concat_result.stderr,
|
||||
)
|
||||
|
||||
# Clean up chunk files and concat list
|
||||
for chunk_path in chunk_files:
|
||||
if chunk_path.exists():
|
||||
chunk_path.unlink()
|
||||
concat_list.unlink(missing_ok=True)
|
||||
|
||||
# Remove chunks directory if empty
|
||||
try:
|
||||
scratch_dir.rmdir()
|
||||
|
||||
+22
-14
@@ -32,7 +32,11 @@ except ImportError:
|
||||
print("Error: 'requests' package is required. Run: pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
SYNC_FILE = ".gnommo_sync.json"
|
||||
SYNC_FILE_LOCAL = ".gnommo_sync.json"
|
||||
SYNC_FILE_PROD = ".gnommo_sync.prod.json"
|
||||
|
||||
def _sync_file(prod: bool) -> str:
|
||||
return SYNC_FILE_PROD if prod else SYNC_FILE_LOCAL
|
||||
|
||||
|
||||
def _load_env_file():
|
||||
@@ -51,16 +55,16 @@ def _load_env_file():
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def _read_sync(project_path: Path) -> dict:
|
||||
sync_file = project_path / SYNC_FILE
|
||||
def _read_sync(project_path: Path, prod: bool = False) -> dict:
|
||||
sync_file = project_path / _sync_file(prod)
|
||||
if sync_file.exists():
|
||||
with open(sync_file) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def _write_sync(project_path: Path, data: dict):
|
||||
with open(project_path / SYNC_FILE, "w") as f:
|
||||
def _write_sync(project_path: Path, data: dict, prod: bool = False):
|
||||
with open(project_path / _sync_file(prod), "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
@@ -106,7 +110,7 @@ def cmd_pull(project_path: Path, verbose: bool = False, force: bool = False, pro
|
||||
|
||||
# ── Conflict check ────────────────────────────────────────────────────────
|
||||
if not force:
|
||||
sync = _read_sync(project_path)
|
||||
sync = _read_sync(project_path, prod)
|
||||
last_pushed_at = _parse_ts(sync.get("last_pushed_at"))
|
||||
local_mtime = datetime.fromtimestamp(
|
||||
project_file.stat().st_mtime, tz=timezone.utc
|
||||
@@ -134,15 +138,19 @@ def cmd_pull(project_path: Path, verbose: bool = False, force: bool = False, pro
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=30,
|
||||
)
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"Error: Could not connect to {api_url}", file=sys.stderr)
|
||||
print(f"✗ Could not connect to {api_url}")
|
||||
return 1
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 404:
|
||||
print(f"Error: Project '{project_id}' not found on server. Push it first.", file=sys.stderr)
|
||||
|
||||
if not r.ok:
|
||||
if r.status_code == 404:
|
||||
print(f"✗ Project '{project_id}' not found on server. Push it first.")
|
||||
else:
|
||||
print(f"Error: Server returned {e.response.status_code}", file=sys.stderr)
|
||||
try:
|
||||
body = r.json()
|
||||
except Exception:
|
||||
body = r.text[:500]
|
||||
print(f"✗ Server returned {r.status_code}: {body}")
|
||||
return 1
|
||||
|
||||
server = r.json()
|
||||
@@ -167,13 +175,13 @@ def cmd_pull(project_path: Path, verbose: bool = False, force: bool = False, pro
|
||||
f.write("\n")
|
||||
|
||||
now_iso = datetime.now(tz=timezone.utc).isoformat(timespec="seconds")
|
||||
existing_sync = _read_sync(project_path)
|
||||
existing_sync = _read_sync(project_path, prod)
|
||||
_write_sync(project_path, {
|
||||
**existing_sync,
|
||||
"last_pulled_at": now_iso,
|
||||
"server_updated_at": server_updated_at,
|
||||
"last_pushed_at": existing_sync.get("last_pushed_at"),
|
||||
})
|
||||
}, prod)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
+38
-46
@@ -45,7 +45,11 @@ except ImportError:
|
||||
print("Error: 'requests' package is required. Run: pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
SYNC_FILE = ".gnommo_sync.json"
|
||||
SYNC_FILE_LOCAL = ".gnommo_sync.json"
|
||||
SYNC_FILE_PROD = ".gnommo_sync.prod.json"
|
||||
|
||||
def _sync_file(prod: bool) -> str:
|
||||
return SYNC_FILE_PROD if prod else SYNC_FILE_LOCAL
|
||||
|
||||
|
||||
def _load_env_file():
|
||||
@@ -64,16 +68,16 @@ def _load_env_file():
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def _read_sync(project_path: Path) -> dict:
|
||||
sync_file = project_path / SYNC_FILE
|
||||
def _read_sync(project_path: Path, prod: bool = False) -> dict:
|
||||
sync_file = project_path / _sync_file(prod)
|
||||
if sync_file.exists():
|
||||
with open(sync_file) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def _write_sync(project_path: Path, data: dict):
|
||||
with open(project_path / SYNC_FILE, "w") as f:
|
||||
def _write_sync(project_path: Path, data: dict, prod: bool = False):
|
||||
with open(project_path / _sync_file(prod), "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
@@ -120,34 +124,6 @@ def cmd_push(project_path: Path, verbose: bool = False, force: bool = False, pro
|
||||
|
||||
parent_project = project.get("parent_project")
|
||||
|
||||
# ── Conflict check ────────────────────────────────────────────────────────
|
||||
if not force:
|
||||
sync = _read_sync(project_path)
|
||||
recorded_server_ts = _parse_ts(sync.get("server_updated_at"))
|
||||
if recorded_server_ts:
|
||||
try:
|
||||
r_check = requests.get(
|
||||
f"{api_url}/api/projects/{project_id}",
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=10,
|
||||
)
|
||||
if r_check.status_code == 200:
|
||||
current_server_ts = _parse_ts(r_check.json().get("updated_at"))
|
||||
if current_server_ts and current_server_ts > recorded_server_ts:
|
||||
print(
|
||||
f"⚠ Server has changes since your last sync "
|
||||
f"({current_server_ts.strftime('%Y-%m-%d %H:%M')} > "
|
||||
f"{recorded_server_ts.strftime('%Y-%m-%d %H:%M')}).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
" Pull first with `gnommo pull -p` or use `gnommo push -p --force`.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
except requests.exceptions.ConnectionError:
|
||||
pass
|
||||
|
||||
# ── Build payload ─────────────────────────────────────────────────────────
|
||||
if parent_project:
|
||||
payload = _build_short_payload(project, project_path, verbose)
|
||||
@@ -166,16 +142,16 @@ def cmd_push(project_path: Path, verbose: bool = False, force: bool = False, pro
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=30,
|
||||
)
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"Error: Could not connect to {api_url}", file=sys.stderr)
|
||||
print(f"✗ Could not connect to {api_url}")
|
||||
return 1
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"Error: Server returned {e.response.status_code}", file=sys.stderr)
|
||||
|
||||
if not r.ok:
|
||||
try:
|
||||
print(f" {e.response.json()}", file=sys.stderr)
|
||||
body = r.json()
|
||||
except Exception:
|
||||
pass
|
||||
body = r.text[:500]
|
||||
print(f"✗ Server returned {r.status_code}: {body}")
|
||||
return 1
|
||||
|
||||
result = r.json()
|
||||
@@ -183,12 +159,12 @@ def cmd_push(project_path: Path, verbose: bool = False, force: bool = False, pro
|
||||
|
||||
# ── Write sync state ──────────────────────────────────────────────────────
|
||||
now_iso = datetime.now(tz=timezone.utc).isoformat(timespec="seconds")
|
||||
existing_sync = _read_sync(project_path)
|
||||
existing_sync = _read_sync(project_path, prod)
|
||||
_write_sync(project_path, {
|
||||
**existing_sync,
|
||||
"last_pushed_at": now_iso,
|
||||
"server_updated_at": server_updated_at,
|
||||
})
|
||||
}, prod)
|
||||
|
||||
# ── Print summary ─────────────────────────────────────────────────────────
|
||||
asset = result.get("asset", {})
|
||||
@@ -198,6 +174,9 @@ def cmd_push(project_path: Path, verbose: bool = False, force: bool = False, pro
|
||||
print(f" task #{result['task_id']} created")
|
||||
else:
|
||||
print(f"✓ {project_id} → gn_asset #{asset.get('id')} ({asset.get('name')})")
|
||||
if verbose:
|
||||
script_len = len(asset.get("script") or "")
|
||||
print(f" server.script: {script_len} chars | fps={asset.get('fps')} res={asset.get('resolution')}")
|
||||
|
||||
return 0
|
||||
|
||||
@@ -209,11 +188,17 @@ def _build_parent_payload(project: dict, project_path: Path, verbose: bool) -> d
|
||||
if manuscript_str:
|
||||
manuscript_path = project_path / manuscript_str
|
||||
if manuscript_path.exists():
|
||||
script_content = manuscript_path.read_text()
|
||||
if verbose:
|
||||
print(f" Read manuscript: {manuscript_path} ({len(script_content)} chars)")
|
||||
try:
|
||||
script_content = manuscript_path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
script_content = manuscript_path.read_text(encoding="latin-1")
|
||||
print(f" Warning: manuscript is not UTF-8, read as latin-1")
|
||||
print(f" manuscript: {len(script_content)} chars")
|
||||
else:
|
||||
print(f" Warning: manuscript file not found: {manuscript_path}", file=sys.stderr)
|
||||
print(f" Warning: manuscript not found: {manuscript_path}")
|
||||
else:
|
||||
if verbose:
|
||||
print(f" no manuscript field in project.json")
|
||||
|
||||
return {
|
||||
"project_id": project["id"],
|
||||
@@ -221,7 +206,14 @@ def _build_parent_payload(project: dict, project_path: Path, verbose: bool) -> d
|
||||
"description": project.get("description"),
|
||||
"coursecode": project.get("coursecode"),
|
||||
"script_content": script_content,
|
||||
"shorts": project.get("shorts", []), # list of slugs, not objects
|
||||
"resolution": project.get("resolution"),
|
||||
"fps": project.get("fps"),
|
||||
"duration_seconds": project.get("duration_seconds"),
|
||||
"hook": project.get("hook"),
|
||||
"platform_targets": project.get("platform_targets"),
|
||||
"status": project.get("status"),
|
||||
"youtube_url": project.get("youtube_url"),
|
||||
"shorts": project.get("shorts", []),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user