mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-08-28 23:27:04 +08:00
Document preview protocol and Audacity autosave
Add the preview bundle protocol plan, record video matrix review evidence, and make one-shot Audacity project mutations persist to disk with E2E coverage.
This commit is contained in:
@@ -40,6 +40,7 @@ from cli_anything.audacity.core import export as export_mod
|
||||
_session: Optional[Session] = None
|
||||
_json_output = False
|
||||
_repl_mode = False
|
||||
_dry_run = False
|
||||
|
||||
|
||||
def get_session() -> Session:
|
||||
@@ -63,6 +64,15 @@ def output(data, message: str = ""):
|
||||
click.echo(str(data))
|
||||
|
||||
|
||||
def autosave_session_if_needed() -> None:
|
||||
"""Persist one-shot mutations immediately when working from a project file."""
|
||||
if _repl_mode or _dry_run:
|
||||
return
|
||||
sess = get_session()
|
||||
if sess.has_project() and sess._modified and sess.project_path:
|
||||
sess.save_session()
|
||||
|
||||
|
||||
def _print_dict(d: dict, indent: int = 0):
|
||||
prefix = " " * indent
|
||||
for k, v in d.items():
|
||||
@@ -129,8 +139,9 @@ def cli(ctx, use_json, project_path, dry_run):
|
||||
|
||||
Run without a subcommand to enter interactive REPL mode.
|
||||
"""
|
||||
global _json_output
|
||||
global _json_output, _dry_run
|
||||
_json_output = use_json
|
||||
_dry_run = dry_run
|
||||
|
||||
if project_path:
|
||||
sess = get_session()
|
||||
@@ -229,6 +240,7 @@ def project_settings(sample_rate, bit_depth, channels):
|
||||
sess.snapshot("Change settings")
|
||||
result = proj_mod.set_settings(proj, sample_rate, bit_depth, channels)
|
||||
output(result, "Settings updated:")
|
||||
autosave_session_if_needed()
|
||||
else:
|
||||
output(proj.get("settings", {}), "Project settings:")
|
||||
|
||||
@@ -264,6 +276,7 @@ def track_add(name, track_type, volume, pan):
|
||||
volume=volume, pan=pan,
|
||||
)
|
||||
output(result, f"Added track: {result['name']}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@track.command("remove")
|
||||
@@ -275,6 +288,7 @@ def track_remove(index):
|
||||
sess.snapshot(f"Remove track {index}")
|
||||
removed = track_mod.remove_track(sess.get_project(), index)
|
||||
output(removed, f"Removed track: {removed.get('name', '')}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@track.command("list")
|
||||
@@ -298,6 +312,7 @@ def track_set(index, prop, value):
|
||||
result = track_mod.set_track_property(sess.get_project(), index, prop, value)
|
||||
output({"track": index, "property": prop, "value": value},
|
||||
f"Set track {index} {prop} = {value}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
# -- Clip Commands ---------------------------------------------------------
|
||||
@@ -336,6 +351,7 @@ def clip_add(track_index, source, name, start, end, trim_start, trim_end, volume
|
||||
trim_start=trim_start, trim_end=trim_end, volume=volume,
|
||||
)
|
||||
output(result, f"Added clip: {result['name']}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@clip.command("remove")
|
||||
@@ -348,6 +364,7 @@ def clip_remove(track_index, clip_index):
|
||||
sess.snapshot(f"Remove clip {clip_index} from track {track_index}")
|
||||
removed = clip_mod.remove_clip(sess.get_project(), track_index, clip_index)
|
||||
output(removed, f"Removed clip: {removed.get('name', '')}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@clip.command("trim")
|
||||
@@ -365,6 +382,7 @@ def clip_trim(track_index, clip_index, trim_start, trim_end):
|
||||
trim_start=trim_start, trim_end=trim_end,
|
||||
)
|
||||
output(result, "Clip trimmed")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@clip.command("split")
|
||||
@@ -380,6 +398,7 @@ def clip_split(track_index, clip_index, split_time):
|
||||
sess.get_project(), track_index, clip_index, split_time,
|
||||
)
|
||||
output(result, f"Split clip into 2 parts at {split_time}s")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@clip.command("move")
|
||||
@@ -395,6 +414,7 @@ def clip_move(track_index, clip_index, new_start):
|
||||
sess.get_project(), track_index, clip_index, new_start,
|
||||
)
|
||||
output(result, f"Moved clip to {new_start}s")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@clip.command("list")
|
||||
@@ -455,6 +475,7 @@ def effect_add(name, track_index, param):
|
||||
sess.snapshot(f"Add effect {name} to track {track_index}")
|
||||
result = fx_mod.add_effect(sess.get_project(), name, track_index, params)
|
||||
output(result, f"Added effect: {name}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@effect_group.command("remove")
|
||||
@@ -467,6 +488,7 @@ def effect_remove(effect_index, track_index):
|
||||
sess.snapshot(f"Remove effect {effect_index} from track {track_index}")
|
||||
result = fx_mod.remove_effect(sess.get_project(), effect_index, track_index)
|
||||
output(result, f"Removed effect {effect_index}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@effect_group.command("set")
|
||||
@@ -486,6 +508,7 @@ def effect_set(effect_index, param, value, track_index):
|
||||
fx_mod.set_effect_param(sess.get_project(), effect_index, param, value, track_index)
|
||||
output({"effect": effect_index, "param": param, "value": value},
|
||||
f"Set effect {effect_index} {param} = {value}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@effect_group.command("list")
|
||||
@@ -514,6 +537,7 @@ def selection_set(start, end):
|
||||
sess = get_session()
|
||||
result = sel_mod.set_selection(sess.get_project(), start, end)
|
||||
output(result, f"Selection: {start}s - {end}s")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@selection.command("all")
|
||||
@@ -523,6 +547,7 @@ def selection_all():
|
||||
sess = get_session()
|
||||
result = sel_mod.select_all(sess.get_project())
|
||||
output(result, "Selected all")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@selection.command("none")
|
||||
@@ -532,6 +557,7 @@ def selection_none():
|
||||
sess = get_session()
|
||||
result = sel_mod.select_none(sess.get_project())
|
||||
output(result, "Selection cleared")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@selection.command("info")
|
||||
@@ -561,6 +587,7 @@ def label_add(start, end, text):
|
||||
sess.snapshot(f"Add label at {start}")
|
||||
result = label_mod.add_label(sess.get_project(), start, end, text)
|
||||
output(result, f"Added label: {text or f'at {start}s'}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@label.command("remove")
|
||||
@@ -572,6 +599,7 @@ def label_remove(index):
|
||||
sess.snapshot(f"Remove label {index}")
|
||||
removed = label_mod.remove_label(sess.get_project(), index)
|
||||
output(removed, f"Removed label: {removed.get('text', '')}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@label.command("list")
|
||||
@@ -671,6 +699,7 @@ def session_undo():
|
||||
sess = get_session()
|
||||
desc = sess.undo()
|
||||
output({"undone": desc}, f"Undone: {desc}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@session_group.command("redo")
|
||||
@@ -680,6 +709,7 @@ def session_redo():
|
||||
sess = get_session()
|
||||
desc = sess.redo()
|
||||
output({"redone": desc}, f"Redone: {desc}")
|
||||
autosave_session_if_needed()
|
||||
|
||||
|
||||
@session_group.command("history")
|
||||
|
||||
@@ -707,6 +707,26 @@ class TestCLISubprocess:
|
||||
assert result.returncode == 0
|
||||
assert "wav" in result.stdout.lower()
|
||||
|
||||
def test_cli_project_mutations_persist_to_disk(self, tmp_dir, sine_wav):
|
||||
project_path = os.path.join(tmp_dir, "persist.json")
|
||||
|
||||
result = self._run_cli(["project", "new", "--name", "Persist", "-o", project_path])
|
||||
assert result.returncode == 0
|
||||
assert os.path.exists(project_path)
|
||||
|
||||
result = self._run_cli(["--project", project_path, "track", "add", "--name", "Music"])
|
||||
assert result.returncode == 0
|
||||
with open(project_path, "r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
assert len(payload["tracks"]) == 1
|
||||
assert payload["tracks"][0]["name"] == "Music"
|
||||
|
||||
result = self._run_cli(["--project", project_path, "clip", "add", "0", sine_wav])
|
||||
assert result.returncode == 0
|
||||
with open(project_path, "r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
assert len(payload["tracks"][0]["clips"]) == 1
|
||||
|
||||
|
||||
# ── True Backend E2E Tests (requires SoX installed) ──────────────
|
||||
|
||||
|
||||
@@ -218,6 +218,8 @@ Recipes declare *which capabilities a workflow needs* — not the order. Apply t
|
||||
|
||||
- **Run the preflight block once**, then consult the cached result when picking providers.
|
||||
- **Search for skills per capability** before starting: `npx skills search "<capability hints>"` (hints are in `matrix_registry.json`).
|
||||
- **For motion-heavy explainer polish, install the Remotion skill inside the workspace:** `npx skills add remotion-dev/skills -a codex -y`. In the reviewed CLI-matrix video scenario, the installed `remotion-best-practices` skill materially improved typography, transition structure, and overflow-safe scene design for reusable title/reveal/diff/end clips.
|
||||
- **For multi-video beat-cut montage work, a strong local recipe is:** extract short source clips into one workspace folder with `ffmpeg`, use the installed Remotion skill to build the typography-heavy motion stings / grid segments, then hand final timeline assembly and grade / envelope filters to `cli-anything-shotcut`. This combination worked well in the reviewed `modern/hype_mix_v1` showcase.
|
||||
- **Prefer `--json`** for harness CLI output when chaining tools.
|
||||
- **Escalate explicitly.** When a paid API would materially improve quality, use the suggest-to-user template. Do not silently burn credits.
|
||||
- **Recipes ≠ order.** A recipe says what's needed; pick a sensible order for the specific task. Most videos should transcribe *after* the final cut, not before; screencasts often capture audio + video simultaneously.
|
||||
|
||||
@@ -43,6 +43,14 @@ Purpose: validate that the S1 capability matrix supports realistic, end-to-end a
|
||||
- [ ] Every provider choice is recorded in `plan.json` with reason
|
||||
- [ ] Agent either used or *explicitly suggested* a paid provider when quality mattered; no silent skips
|
||||
|
||||
**Reviewed Evidence**
|
||||
- [x] Reviewed landscape master: [`landscape_modern_v3_1_shotcut.mp4`](/root/cli-matrix-tests/video-creation-plan1/modern/output/landscape_modern_v3_1_shotcut.mp4)
|
||||
- [x] Reviewed alternate NLE export: [`landscape_modern_v3_1_kdenlive.mp4`](/root/cli-matrix-tests/video-creation-plan1/modern/output/landscape_modern_v3_1_kdenlive.mp4)
|
||||
- [x] Reviewed thumbnail: [`thumb_modern_v3_1.png`](/root/cli-matrix-tests/video-creation-plan1/modern/output/thumb_modern_v3_1.png)
|
||||
- [x] Artifact lineage and rebuild notes: [`modern/AGENTS.md`](/root/cli-matrix-tests/video-creation-plan1/modern/AGENTS.md), [`modern/PROCEDURE.md`](/root/cli-matrix-tests/video-creation-plan1/modern/PROCEDURE.md)
|
||||
- [x] Separate CLI harness usage note: [`CLI_ANYTHING_USAGE_ACTUAL.md`](/root/cli-matrix-tests/video-creation-plan1/modern/CLI_ANYTHING_USAGE_ACTUAL.md)
|
||||
- [x] Additional matrix showcase for multi-video hype montage: [`landscape_hype_mix_v1_shotcut.mp4`](/root/cli-matrix-tests/video-creation-plan1/modern/output/landscape_hype_mix_v1_shotcut.mp4), [`HYPE_MIX_V1.md`](/root/cli-matrix-tests/video-creation-plan1/modern/HYPE_MIX_V1.md)
|
||||
|
||||
---
|
||||
|
||||
## Task 2 — Multilingual developer tutorial (EN → ES/JA/ZH)
|
||||
@@ -153,6 +161,15 @@ Purpose: validate that the S1 capability matrix supports realistic, end-to-end a
|
||||
- [ ] Lyrics appear on the right word at the right time (sample 5 random lines)
|
||||
- [ ] No audio desync at end (lengths match within 50ms)
|
||||
|
||||
**Reviewed Evidence**
|
||||
- [x] Fresh-resource Task 5 rerun reviewed final video: [`landscape_pulse_of_motion_v1_shotcut.mp4`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/output/landscape_pulse_of_motion_v1_shotcut.mp4)
|
||||
- [x] Reviewed fresh-resource thumbnail: [`thumb_pulse_of_motion_v1.png`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/output/thumb_pulse_of_motion_v1.png)
|
||||
- [x] Reviewed fresh-resource Shotcut project and no-subs master: [`landscape_pulse_of_motion_v1_shotcut.mlt`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/build/shotcut/landscape_pulse_of_motion_v1_shotcut.mlt), [`landscape_pulse_of_motion_v1_shotcut_nosubs.mp4`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/build/shotcut/landscape_pulse_of_motion_v1_shotcut_nosubs.mp4)
|
||||
- [x] Reviewed fresh-resource beat map and lyric assets: [`beats.json`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/build/beats.json), [`lyrics.ass`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/build/lyrics.ass), [`lyrics.txt`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/build/lyrics.txt)
|
||||
- [x] Reviewed lyric-window frames after final burn: [`11s_lyric_fix.png`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/review_frames/11s_lyric_fix.png), [`35s_lyric_fix.png`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/review_frames/35s_lyric_fix.png), [`55s_lyric_fix.png`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/review_frames/55s_lyric_fix.png), [`75s_lyric_fix.png`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/review_frames/75s_lyric_fix.png)
|
||||
- [x] Fresh-resource build note and local bug tracking: [`PULSE_OF_MOTION_V1.md`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/PULSE_OF_MOTION_V1.md), [`CLI_bug_track_task5.md`](/root/cli-matrix-tests/video-creation-plan1/task5_pulse_of_motion/CLI_bug_track_task5.md)
|
||||
- [x] Prior LaunchOS-based Task 5 attempt is superseded by this fresh-resource rerun: [`landscape_beat_sync_ai_v1_shotcut.mp4`](/root/cli-matrix-tests/video-creation-plan1/modern/output/landscape_beat_sync_ai_v1_shotcut.mp4)
|
||||
|
||||
---
|
||||
|
||||
## Task 6 — Reaction video (imported clip + webcam + commentary)
|
||||
|
||||
Reference in New Issue
Block a user