mirror of
https://github.com/huggingface/lerobot.git
synced 2026-05-11 14:49:43 +00:00
integrate delete button openarm UI (#2535)
* add visualize_dataset call from `lerobot_dataset_viz` in web record server * add delete button * fixes * remove viz * unused import
This commit is contained in:
@@ -205,6 +205,47 @@ h3 {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.delete-episode-section {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
width: 100%;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.875rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.btn-delete:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-delete:disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.delete-info {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.btn-disconnect {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
|
||||
@@ -21,6 +21,7 @@ function App() {
|
||||
const [rampUpRemaining, setRampUpRemaining] = useState(0);
|
||||
const [movingToZero, setMovingToZero] = useState(false);
|
||||
const [configExpanded, setConfigExpanded] = useState(false);
|
||||
const [latestRepoId, setLatestRepoId] = useState(null);
|
||||
|
||||
// Configuration
|
||||
const [config, setConfig] = useState({
|
||||
@@ -82,6 +83,11 @@ function App() {
|
||||
setUploadStatus(data.upload_status);
|
||||
setRampUpRemaining(data.ramp_up_remaining || 0);
|
||||
setMovingToZero(data.moving_to_zero || false);
|
||||
|
||||
// Track the latest repo_id from the backend
|
||||
if (data.latest_repo_id) {
|
||||
setLatestRepoId(data.latest_repo_id);
|
||||
}
|
||||
|
||||
if (data.config) {
|
||||
// Only merge server config if we don't have a saved config (first load)
|
||||
@@ -308,13 +314,54 @@ function App() {
|
||||
throw new Error(data.detail || 'Failed to stop recording');
|
||||
}
|
||||
|
||||
await response.json();
|
||||
const data = await response.json();
|
||||
setError(null);
|
||||
// Update latest repo_id after recording
|
||||
if (data.dataset_name) {
|
||||
setLatestRepoId(`lerobot-data-collection/${data.dataset_name}`);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteLatestEpisode = async () => {
|
||||
if (!latestRepoId) {
|
||||
setError('No episode to delete');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`WARNING: This will permanently delete the repository:\n\n${latestRepoId}\n\nThis action cannot be undone. Continue?`
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/recording/delete-latest`, { method: 'POST' });
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.detail || 'Failed to delete episode');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setLatestRepoId(null);
|
||||
setEpisodeCount(Math.max(0, episodeCount - 1));
|
||||
setStatusMessage(`Deleted: ${data.deleted_repo}`);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!isRecording && !isInitializing) {
|
||||
setStatusMessage('Ready');
|
||||
}
|
||||
}, 3000);
|
||||
} catch (e) {
|
||||
setError(`Delete failed: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Reset counter
|
||||
const resetCounter = async () => {
|
||||
try {
|
||||
@@ -730,6 +777,20 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Latest Episode Button */}
|
||||
{!isRecording && !isInitializing && latestRepoId && (
|
||||
<div className="delete-episode-section">
|
||||
<button
|
||||
onClick={deleteLatestEpisode}
|
||||
className="btn-delete"
|
||||
title="Delete the latest recorded episode from HuggingFace Hub"
|
||||
>
|
||||
Delete Latest Episode
|
||||
</button>
|
||||
<div className="delete-info">Will delete: {latestRepoId}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Move to Zero Button */}
|
||||
{robotsReady && !isRecording && !isInitializing && (
|
||||
<div className="zero-position-section">
|
||||
|
||||
@@ -9,6 +9,7 @@ import asyncio
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -127,7 +128,6 @@ pedal_thread = None
|
||||
stop_pedal_flag = threading.Event()
|
||||
pedal_action_lock = threading.Lock() # Prevent concurrent pedal actions
|
||||
|
||||
|
||||
class RecordingConfig(BaseModel):
|
||||
task: str
|
||||
leader_type: str # "openarms" or "openarms_mini"
|
||||
@@ -908,7 +908,6 @@ def cleanup_robot_systems(keep_robots=False):
|
||||
# Always clean up dataset
|
||||
robot_instances["dataset"] = None
|
||||
robot_instances["dataset_features"] = None
|
||||
robot_instances["repo_id"] = None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Cleanup] Error: {e}")
|
||||
@@ -1013,6 +1012,7 @@ def do_stop_recording(source: str = "API"):
|
||||
recording_state["status_message"] = "Ready"
|
||||
recording_state["episode_count"] += 1
|
||||
print(f"[{source}] Upload complete. Episode count: {recording_state['episode_count']}")
|
||||
|
||||
else:
|
||||
recording_state["status_message"] = "No data"
|
||||
recording_state["upload_status"] = "No data"
|
||||
@@ -1321,6 +1321,55 @@ async def move_to_zero():
|
||||
return do_move_to_zero(source="API")
|
||||
|
||||
|
||||
@app.post("/api/recording/delete-latest")
|
||||
async def delete_latest_episode():
|
||||
"""Delete the latest recorded episode from HuggingFace Hub."""
|
||||
repo_id = robot_instances.get("repo_id")
|
||||
|
||||
if not repo_id:
|
||||
print(f"[DeleteEpisode] No repository to delete. Please record an episode first.")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No repository to delete. Please record an episode first."
|
||||
}
|
||||
|
||||
try:
|
||||
print(f"[DeleteEpisode] Deleting repository: {repo_id}")
|
||||
|
||||
hf_tool_path = shutil.which("hf")
|
||||
if hf_tool_path is None:
|
||||
print(f"[DeleteEpisode] HuggingFace CLI not found. Please install it.")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "HuggingFace CLI not found. Please install it."
|
||||
}
|
||||
|
||||
subprocess.run(
|
||||
[hf_tool_path, "repo", "delete", repo_id, "--repo-type", "dataset"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
print(f"[DeleteEpisode] Successfully deleted repository: {repo_id}")
|
||||
|
||||
robot_instances["repo_id"] = None
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Successfully deleted repository: {repo_id}",
|
||||
"deleted_repo": repo_id
|
||||
}
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = f"Failed to delete repository: {e.stderr}"
|
||||
print(f"[DeleteEpisode] Error: {error_msg}")
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"Error deleting repository: {str(e)}"
|
||||
print(f"[DeleteEpisode] Error: {error_msg}")
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
|
||||
@app.get("/api/status")
|
||||
async def get_status():
|
||||
"""Get current recording status."""
|
||||
@@ -1345,7 +1394,8 @@ async def get_status():
|
||||
"upload_status": recording_state["upload_status"],
|
||||
"ramp_up_remaining": recording_state["ramp_up_remaining"],
|
||||
"moving_to_zero": recording_state["moving_to_zero"],
|
||||
"config": recording_state["config"]
|
||||
"config": recording_state["config"],
|
||||
"latest_repo_id": robot_instances.get("repo_id")
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user