Skip to content

Commit 5929fdf

Browse files
Jintao MaJintao Ma
authored andcommitted
Update annotation_tool , Add description Tool and replace viewer.py
1 parent 9ad351d commit 5929fdf

15 files changed

Lines changed: 949 additions & 66 deletions

File tree

6 KB
Binary file not shown.

annotation_tool/controllers/description/__init__.py

Whitespace-only changes.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import copy
2+
from PyQt6.QtCore import QModelIndex
3+
from PyQt6.QtWidgets import QMessageBox
4+
from models.project_tree import ProjectTreeModel
5+
6+
class DescAnnotationManager:
7+
"""
8+
Manages data loading and saving for Description Mode (Right Panel).
9+
Handles the formatting of Q&A from JSON and flattening it upon save.
10+
"""
11+
def __init__(self, main_window):
12+
self.main = main_window
13+
self.model = main_window.model
14+
self.ui = main_window.ui.description_ui.right_panel
15+
self.current_action_path = None
16+
17+
def setup_connections(self):
18+
"""Connect UI signals to controller methods."""
19+
# Listen to Tree Selection from the Description Panel
20+
tree = self.main.ui.description_ui.left_panel.tree
21+
tree.selectionModel().currentChanged.connect(self.on_item_selected)
22+
23+
# Connect Editor Buttons
24+
self.ui.confirm_clicked.connect(self.save_current_annotation)
25+
self.ui.clear_clicked.connect(self.clear_current_text)
26+
27+
def on_item_selected(self, current: QModelIndex, previous: QModelIndex):
28+
"""
29+
Triggered when a tree item is selected.
30+
Loads the corresponding data into the text editor.
31+
"""
32+
if not current.isValid():
33+
self.ui.caption_edit.clear()
34+
self.current_action_path = None
35+
self.ui.caption_edit.setEnabled(False)
36+
return
37+
38+
# 1. Identify the Action Path
39+
path = current.data(ProjectTreeModel.FilePathRole)
40+
model = self.main.tree_model
41+
42+
# If user clicked a child (video), find the parent (action) to show shared annotations
43+
if not model.hasChildren(current) and current.parent().isValid():
44+
parent_idx = current.parent()
45+
path = parent_idx.data(ProjectTreeModel.FilePathRole)
46+
47+
self.current_action_path = path
48+
self.ui.caption_edit.setEnabled(True)
49+
50+
# 2. Find Data Object in Model
51+
# We search by path first, fallback to ID if needed
52+
action_data = next((item for item in self.model.action_item_data if item.get("metadata", {}).get("path") == path), None)
53+
if not action_data:
54+
action_data = next((item for item in self.model.action_item_data if item.get("id") == current.text()), None)
55+
56+
if not action_data:
57+
self.ui.caption_edit.setPlaceholderText("No metadata found for this item.")
58+
return
59+
60+
# 3. Format and Display Text
61+
self._load_and_format_text(action_data)
62+
63+
def _load_and_format_text(self, data):
64+
"""
65+
Formats the display text.
66+
- If 'captions' contains 'question' fields, formats as Q&A blocks.
67+
- If 'captions' is plain text (already saved), displays it directly.
68+
"""
69+
captions = data.get("captions", [])
70+
formatted_blocks = []
71+
72+
if captions:
73+
# Iterate through existing captions (which might be raw Q&A or edited text)
74+
for cap in captions:
75+
text = cap.get("text", "")
76+
question = cap.get("question", "") # Check for the 'question' key
77+
78+
if question:
79+
# Format as Q & A if the question key exists
80+
formatted_blocks.append(f'Q: "{question}"\nA: "{text}"')
81+
else:
82+
# Otherwise, just append the text (e.g. for already edited/flattened descriptions)
83+
formatted_blocks.append(text)
84+
85+
full_text = "\n\n".join(formatted_blocks)
86+
87+
else:
88+
# Fallback: If no captions exist yet, try to generate template from metadata questions
89+
metadata = data.get("metadata", {})
90+
questions = metadata.get("questions", [])
91+
for i, q in enumerate(questions):
92+
formatted_blocks.append(f'Q: "{q}"\nA: ""')
93+
94+
full_text = "\n\n".join(formatted_blocks)
95+
96+
self.ui.caption_edit.setPlainText(full_text)
97+
98+
def save_current_annotation(self):
99+
"""
100+
Saves the current text content back to the JSON model.
101+
Flattens the structure: removes 'question' keys and saves everything as one text block.
102+
"""
103+
if not self.current_action_path:
104+
return
105+
106+
text_content = self.ui.caption_edit.toPlainText()
107+
108+
# Find the target data item in the model
109+
target_item = None
110+
for item in self.model.action_item_data:
111+
if item.get("metadata", {}).get("path") == self.current_action_path:
112+
target_item = item
113+
break
114+
115+
if target_item:
116+
# [CRITICAL UPDATE]
117+
# Replace the entire 'captions' list with a single entry containing the full text.
118+
# This effectively removes the old 'question' fields from the JSON structure
119+
# and stores the user's edited content purely as 'text'.
120+
target_item["captions"] = [
121+
{
122+
"lang": "en",
123+
"text": text_content
124+
}
125+
]
126+
127+
# Mark state as dirty so Save button becomes active
128+
self.model.is_data_dirty = True
129+
self.main.update_save_export_button_state()
130+
131+
# Update Tree Icon (Done/Empty)
132+
is_done = bool(text_content.strip())
133+
self._update_tree_icon(self.current_action_path, is_done)
134+
135+
self.main.show_temp_msg("Saved", "Description updated.")
136+
137+
# Auto-advance to next item
138+
self._auto_advance()
139+
140+
def clear_current_text(self):
141+
"""Clears the editor text."""
142+
self.ui.caption_edit.clear()
143+
144+
def _update_tree_icon(self, path, is_done):
145+
"""Updates the checkmark icon in the tree view."""
146+
item = self.model.action_item_map.get(path)
147+
if item:
148+
item.setIcon(self.main.done_icon if is_done else self.main.empty_icon)
149+
150+
def _auto_advance(self):
151+
"""Moves selection to the next Action in the tree automatically."""
152+
self.main.desc_nav_manager.nav_next_action()
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import json
2+
import os
3+
from PyQt6.QtWidgets import QFileDialog, QMessageBox
4+
5+
class DescFileManager:
6+
"""
7+
Handles JSON I/O for the Description / Video Captioning mode.
8+
Responsible for populating the ProjectTreeModel and saving data back to disk.
9+
"""
10+
def __init__(self, main_window):
11+
self.main = main_window
12+
self.model = main_window.model # AppStateModel
13+
14+
def create_new_project(self):
15+
"""
16+
Initializes a fresh Description project state.
17+
"""
18+
# 1. Reset Global State
19+
self._clear_workspace(full_reset=True)
20+
21+
# 2. Set Task Metadata
22+
self.model.current_task_name = "video_captioning"
23+
self.model.json_loaded = True
24+
self.model.is_data_dirty = True
25+
self.model.current_working_directory = None
26+
self.model.current_json_path = None
27+
28+
# 3. Setup UI for blank state
29+
self.main.prepare_new_description_ui()
30+
self.main.ui.show_description_view()
31+
self.main.update_save_export_button_state()
32+
33+
self.main.show_temp_msg("New Project", "Description project created. Use 'Add Data' to start.")
34+
35+
def load_project(self, data: dict, file_path: str):
36+
"""
37+
Loads the JSON data into the Model and populates the Tree View.
38+
"""
39+
self._clear_workspace(full_reset=False)
40+
self.model.current_json_path = file_path
41+
self.model.current_working_directory = os.path.dirname(file_path)
42+
43+
# 1. Store Raw Data
44+
self.model.action_item_data = data.get("data", [])
45+
self.model.current_task_name = data.get("task", "video_captioning")
46+
47+
# 2. Populate the Tree
48+
self._populate_tree()
49+
50+
# 3. Finalize UI
51+
self.model.json_loaded = True
52+
self.model.is_data_dirty = False
53+
self.main.setWindowTitle(f"SoccerNet Pro - {os.path.basename(file_path)}")
54+
self.main.update_save_export_button_state()
55+
56+
self.main.show_temp_msg("Loaded", f"Loaded {len(self.model.action_item_data)} actions.")
57+
58+
def save_json(self) -> bool:
59+
"""Saves the current state back to JSON."""
60+
if not self.model.current_json_path:
61+
return self.save_as_json()
62+
63+
# Construct data dictionary
64+
output_data = {
65+
"version": "1.0",
66+
"task": self.model.current_task_name,
67+
"data": self.model.action_item_data
68+
}
69+
# Preserve metadata if available in model (optional expansion)
70+
71+
try:
72+
with open(self.model.current_json_path, 'w', encoding='utf-8') as f:
73+
json.dump(output_data, f, indent=2, ensure_ascii=False)
74+
75+
self.model.is_data_dirty = False
76+
self.main.update_save_export_button_state()
77+
self.main.show_temp_msg("Saved", "Project saved successfully.")
78+
return True
79+
except Exception as e:
80+
QMessageBox.critical(self.main, "Save Error", f"Failed to save JSON:\n{e}")
81+
return False
82+
83+
def save_as_json(self) -> bool:
84+
path, _ = QFileDialog.getSaveFileName(
85+
self.main, "Save Project As", "", "JSON Files (*.json)"
86+
)
87+
if not path:
88+
return False
89+
90+
self.model.current_json_path = path
91+
return self.save_json()
92+
93+
def export_json(self):
94+
"""Export logic (currently same as save as)."""
95+
self.save_as_json()
96+
97+
def _populate_tree(self):
98+
"""
99+
Converts JSON structure into the QStandardItemModel.
100+
"""
101+
tree_model = self.main.tree_model
102+
tree_model.clear()
103+
self.model.action_item_map.clear()
104+
105+
# Iterate through the JSON 'data' list
106+
for item in self.model.action_item_data:
107+
# A. Get Display Name (Action ID)
108+
action_id = item.get("id", "Unknown ID")
109+
110+
# B. Get Metadata Path
111+
action_path = item.get("metadata", {}).get("path", action_id)
112+
113+
# C. Extract Video Paths from 'inputs' list
114+
input_paths = []
115+
inputs = item.get("inputs", [])
116+
for inp in inputs:
117+
if isinstance(inp, dict) and "path" in inp:
118+
input_paths.append(inp["path"])
119+
120+
# D. Add to Tree Model
121+
tree_item = tree_model.add_entry(
122+
name=action_id,
123+
path=action_path,
124+
source_files=input_paths
125+
)
126+
127+
# Store mapping
128+
self.model.action_item_map[action_path] = tree_item
129+
130+
# E. Check Status
131+
captions = item.get("captions", [])
132+
has_content = len(captions) > 0 and bool(captions[0].get("text", "").strip())
133+
134+
if has_content:
135+
tree_item.setIcon(self.main.done_icon)
136+
else:
137+
tree_item.setIcon(self.main.empty_icon)
138+
139+
def _clear_workspace(self, full_reset=True):
140+
"""Clears the tree and model state."""
141+
self.main.tree_model.clear()
142+
self.model.action_item_map.clear()
143+
if full_reset:
144+
self.model.json_loaded = False
145+
self.model.current_json_path = None
146+
self.model.action_item_data = []
147+
self.model.is_data_dirty = False
148+
self.main.ui.description_ui.right_panel.caption_edit.clear()
149+
self.main.ui.description_ui.right_panel.caption_edit.setEnabled(False)

0 commit comments

Comments
 (0)