(
self,
nodes: List[str],
final_video_path: str,
node_output_dir: str,
characters_text: str,
scene_text: str,
station_nodes: List[str],
time_spans: Optional[List[Tuple[int, int]]] = None,
style_slug: Optional[str] = None,
resume_from: int = 1, # ✅ New: Which node to start generation from
resume_reference_path: Optional[str] = None, # ✅ New: Reference image path when resuming generation
)
| 1793 | |
| 1794 | |
| 1795 | def generate( |
| 1796 | self, |
| 1797 | nodes: List[str], |
| 1798 | final_video_path: str, |
| 1799 | node_output_dir: str, |
| 1800 | characters_text: str, |
| 1801 | scene_text: str, |
| 1802 | station_nodes: List[str], |
| 1803 | time_spans: Optional[List[Tuple[int, int]]] = None, |
| 1804 | style_slug: Optional[str] = None, |
| 1805 | resume_from: int = 1, # ✅ New: Which node to start generation from |
| 1806 | resume_reference_path: Optional[str] = None, # ✅ New: Reference image path when resuming generation |
| 1807 | ) -> None: |
| 1808 | if not nodes: |
| 1809 | raise ValueError("Script nodes are empty, cannot generate video.") |
| 1810 | |
| 1811 | self._validate_params() |
| 1812 | |
| 1813 | # ✅ If resume reference image is provided, use it directly |
| 1814 | if resume_reference_path and os.path.exists(resume_reference_path): |
| 1815 | LOGGER.info(f"Using resume generation mode, reference image: {resume_reference_path}") |
| 1816 | self._reference_spec = ReferenceImageSpec( |
| 1817 | path=resume_reference_path, |
| 1818 | mode=self.reference_mode |
| 1819 | ) |
| 1820 | else: |
| 1821 | self._reference_spec = None |
| 1822 | |
| 1823 | os.makedirs(node_output_dir, exist_ok=True) |
| 1824 | os.makedirs(os.path.dirname(final_video_path), exist_ok=True) |
| 1825 | |
| 1826 | # ✅ Mechanism 2: Auto-resume - Check if final video already exists |
| 1827 | if os.path.exists(final_video_path): |
| 1828 | file_size = os.path.getsize(final_video_path) |
| 1829 | if file_size > 0: |
| 1830 | LOGGER.info(f"✅ Final video already exists, skipping generation: {final_video_path} ({file_size / 1024 / 1024:.2f} MB)") |
| 1831 | return nodes, station_nodes, False # ✅ Return original data, no nodes deleted |
| 1832 | else: |
| 1833 | LOGGER.warning(f"Detected empty final video file, will regenerate: {final_video_path}") |
| 1834 | os.remove(final_video_path) |
| 1835 | |
| 1836 | # ✅ Mechanism 1: Dynamic node list (supports skipping failed nodes) |
| 1837 | active_nodes = list(nodes) |
| 1838 | active_stations = list(station_nodes or []) |
| 1839 | active_time_spans = list(time_spans or []) |
| 1840 | nodes_deleted = False # ✅ Mark if any nodes were deleted |
| 1841 | |
| 1842 | total = len(active_nodes) |
| 1843 | video_paths: List[str] = [] |
| 1844 | |
| 1845 | # ✅ Detect existing video files |
| 1846 | existing_videos, _, _ = detect_existing_nodes(node_output_dir) |
| 1847 | video_paths.extend(existing_videos) |
| 1848 | |
| 1849 | station_sequence = list(active_stations) |
| 1850 | if len(station_sequence) < total: |
| 1851 | filler = station_sequence[-1] if station_sequence else "Station information to be supplemented" |
| 1852 | station_sequence.extend([filler] * (total - len(station_sequence))) |
no test coverage detected