os_util.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. # Copyright (C) 2025 AIDC-AI
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. """
  13. OS utilities for file and path management
  14. Provides utilities for managing paths and files in Pixelle-Video.
  15. Inspired by Pixelle-MCP's os_util.py.
  16. """
  17. import os
  18. import random
  19. from datetime import datetime
  20. from pathlib import Path
  21. from typing import Optional, Tuple, Literal
  22. def get_pixelle_video_root_path() -> str:
  23. """
  24. Get Pixelle-Video root path
  25. Uses PIXELLE_VIDEO_ROOT environment variable to determine project root.
  26. This ensures reliable path resolution in both development and packaged environments.
  27. Returns:
  28. Project root path as string
  29. """
  30. # Check environment variable (required for reliable operation)
  31. env_root = os.environ.get("PIXELLE_VIDEO_ROOT")
  32. if env_root and Path(env_root).exists():
  33. return str(Path(env_root).resolve())
  34. # Fallback to current working directory if environment variable not set
  35. # (for development environments where env var might not be set)
  36. return str(Path.cwd())
  37. def ensure_pixelle_video_root_path() -> str:
  38. """
  39. Ensure Pixelle-Video root path exists and return the path
  40. Returns:
  41. Root path as string
  42. """
  43. root_path = get_pixelle_video_root_path()
  44. root_path_obj = Path(root_path)
  45. output_dir = root_path_obj / 'output'
  46. output_dir.mkdir(parents=True, exist_ok=True)
  47. return root_path
  48. def get_root_path(*paths: str) -> str:
  49. """
  50. Get path relative to Pixelle-Video root path
  51. Args:
  52. *paths: Path components to join
  53. Returns:
  54. Absolute path as string
  55. Example:
  56. get_root_path("temp", "audio.mp3")
  57. # Returns: "/path/to/project/temp/audio.mp3"
  58. """
  59. root_path = ensure_pixelle_video_root_path()
  60. if paths:
  61. return os.path.join(root_path, *paths)
  62. return root_path
  63. def get_temp_path(*paths: str) -> str:
  64. """
  65. Get path relative to Pixelle-Video temp folder
  66. Ensures temp directory exists before returning path.
  67. Args:
  68. *paths: Path components to join
  69. Returns:
  70. Absolute path to temp directory or file
  71. Example:
  72. get_temp_path("audio.mp3")
  73. # Returns: "/path/to/project/temp/audio.mp3"
  74. """
  75. temp_path = get_root_path("temp")
  76. # Ensure temp directory exists
  77. os.makedirs(temp_path, exist_ok=True)
  78. if paths:
  79. return os.path.join(temp_path, *paths)
  80. return temp_path
  81. def get_data_path(*paths: str) -> str:
  82. """
  83. Get path relative to Pixelle-Video data folder
  84. Ensures data directory exists before returning path.
  85. Args:
  86. *paths: Path components to join
  87. Returns:
  88. Absolute path to data directory or file
  89. Example:
  90. get_data_path("videos", "output.mp4")
  91. # Returns: "/path/to/project/data/videos/output.mp4"
  92. """
  93. data_path = get_root_path("data")
  94. # Ensure data directory exists
  95. os.makedirs(data_path, exist_ok=True)
  96. if paths:
  97. return os.path.join(data_path, *paths)
  98. return data_path
  99. def get_output_path(*paths: str) -> str:
  100. """
  101. Get path relative to Pixelle-Video output folder
  102. Ensures output directory exists before returning path.
  103. Args:
  104. *paths: Path components to join
  105. Returns:
  106. Absolute path to output directory or file
  107. Example:
  108. get_output_path("video.mp4")
  109. # Returns: "/path/to/project/output/video.mp4"
  110. """
  111. output_path = get_root_path("output")
  112. # Ensure output directory exists
  113. os.makedirs(output_path, exist_ok=True)
  114. if paths:
  115. return os.path.join(output_path, *paths)
  116. return output_path
  117. def save_bytes_to_file(data: bytes, file_path: str) -> str:
  118. """
  119. Save bytes data to file
  120. Creates parent directories if they don't exist.
  121. Args:
  122. data: Binary data to save
  123. file_path: Target file path
  124. Returns:
  125. Absolute path of saved file
  126. Example:
  127. save_bytes_to_file(audio_data, get_temp_path("audio.mp3"))
  128. """
  129. # Ensure parent directory exists
  130. os.makedirs(os.path.dirname(file_path), exist_ok=True)
  131. # Write binary data
  132. with open(file_path, "wb") as f:
  133. f.write(data)
  134. return os.path.abspath(file_path)
  135. def ensure_dir(path: str) -> str:
  136. """
  137. Ensure directory exists, create if not
  138. Args:
  139. path: Directory path
  140. Returns:
  141. Absolute path of directory
  142. """
  143. os.makedirs(path, exist_ok=True)
  144. return os.path.abspath(path)
  145. # ========== Task Directory Management ==========
  146. def create_task_id() -> str:
  147. """
  148. Create unique task ID with timestamp + random suffix
  149. Format: {timestamp}_{random_hex}
  150. Example: "20251028_143052_ab3d"
  151. Collision probability: < 0.0001% (65536 combinations per second)
  152. Returns:
  153. Task ID string
  154. """
  155. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  156. random_suffix = f"{random.randint(0, 0xFFFF):04x}" # 4-digit hex (0000-ffff)
  157. return f"{timestamp}_{random_suffix}"
  158. def create_task_output_dir(task_id: Optional[str] = None) -> Tuple[str, str]:
  159. """
  160. Create isolated output directory for single video generation task
  161. Directory structure:
  162. output/{task_id}/
  163. ├── final.mp4 # Final video output
  164. ├── frames/ # All frame-related files
  165. │ ├── 01_audio.mp3
  166. │ ├── 01_image.png
  167. │ ├── 01_composed.png
  168. │ ├── 01_segment.mp4
  169. │ └── ...
  170. └── metadata.json # Optional: task metadata
  171. Args:
  172. task_id: Optional task ID (auto-generated if None)
  173. Returns:
  174. (task_dir, task_id) tuple
  175. Example:
  176. >>> task_dir, task_id = create_task_output_dir()
  177. >>> # task_dir = "/path/to/project/output/20251028_143052_ab3d"
  178. >>> # task_id = "20251028_143052_ab3d"
  179. """
  180. if task_id is None:
  181. task_id = create_task_id()
  182. task_dir = get_output_path(task_id)
  183. frames_dir = os.path.join(task_dir, "frames")
  184. # Create directories
  185. os.makedirs(frames_dir, exist_ok=True)
  186. return task_dir, task_id
  187. def get_task_path(task_id: str, *paths: str) -> str:
  188. """
  189. Get path within task directory
  190. Args:
  191. task_id: Task ID
  192. *paths: Path components to join
  193. Returns:
  194. Absolute path within task directory
  195. Example:
  196. >>> get_task_path("20251028_143052_ab3d", "final.mp4")
  197. >>> # Returns: "/path/to/project/output/20251028_143052_ab3d/final.mp4"
  198. """
  199. task_dir = get_output_path(task_id)
  200. if paths:
  201. return os.path.join(task_dir, *paths)
  202. return task_dir
  203. def get_task_frame_path(
  204. task_id: str,
  205. frame_index: int,
  206. file_type: Literal["audio", "image", "video", "composed", "segment"]
  207. ) -> str:
  208. """
  209. Get frame file path within task directory
  210. Args:
  211. task_id: Task ID
  212. frame_index: Frame index (0-based internally, but filename starts from 01)
  213. file_type: File type (audio/image/video/composed/segment)
  214. Returns:
  215. Absolute path to frame file
  216. Example:
  217. >>> get_task_frame_path("20251028_143052_ab3d", 0, "audio")
  218. >>> # Returns: ".../output/20251028_143052_ab3d/frames/01_audio.mp3"
  219. """
  220. ext_map = {
  221. "audio": "mp3",
  222. "image": "png",
  223. "video": "mp4",
  224. "composed": "png",
  225. "segment": "mp4"
  226. }
  227. # Frame number starts from 01 for better human readability
  228. filename = f"{frame_index + 1:02d}_{file_type}.{ext_map[file_type]}"
  229. return get_task_path(task_id, "frames", filename)
  230. def get_task_final_video_path(task_id: str) -> str:
  231. """
  232. Get final video path within task directory
  233. Args:
  234. task_id: Task ID
  235. Returns:
  236. Absolute path to final video
  237. Example:
  238. >>> get_task_final_video_path("20251028_143052_ab3d")
  239. >>> # Returns: ".../output/20251028_143052_ab3d/final.mp4"
  240. """
  241. return get_task_path(task_id, "final.mp4")
  242. # ========== Resource Management (Templates/BGM/Workflows) ==========
  243. def get_resource_path(resource_type: Literal["bgm", "templates", "workflows"], *paths: str) -> str:
  244. """
  245. Get resource file path with custom override support
  246. Search priority:
  247. 1. data/{resource_type}/*paths (custom, higher priority)
  248. 2. {resource_type}/*paths (default, fallback)
  249. Args:
  250. resource_type: Resource type ("bgm", "templates", "workflows")
  251. *paths: Path components relative to resource directory
  252. Returns:
  253. Absolute path to resource file (custom if exists, otherwise default)
  254. Raises:
  255. FileNotFoundError: If file not found in either location
  256. Examples:
  257. >>> get_resource_path("bgm", "happy.mp3")
  258. # Returns: "data/bgm/happy.mp3" (if exists) or "bgm/happy.mp3"
  259. >>> get_resource_path("templates", "1080x1920", "default.html")
  260. # Returns: "data/templates/1080x1920/default.html" or "templates/1080x1920/default.html"
  261. >>> get_resource_path("workflows", "selfhost", "image_flux.json")
  262. # Returns: "data/workflows/selfhost/image_flux.json" or "workflows/selfhost/image_flux.json"
  263. """
  264. # Build custom path (data/*)
  265. custom_path = get_data_path(resource_type, *paths)
  266. # Build default path (root/*)
  267. default_path = get_root_path(resource_type, *paths)
  268. # Priority: custom > default
  269. if os.path.exists(custom_path):
  270. return custom_path
  271. if os.path.exists(default_path):
  272. return default_path
  273. # Not found in either location
  274. raise FileNotFoundError(
  275. f"Resource not found: {os.path.join(resource_type, *paths)}\n"
  276. f" Searched locations:\n"
  277. f" 1. {custom_path} (custom)\n"
  278. f" 2. {default_path} (default)"
  279. )
  280. def list_resource_files(
  281. resource_type: Literal["bgm", "templates", "workflows"],
  282. subdir: str = ""
  283. ) -> list[str]:
  284. """
  285. List resource files with custom override support
  286. Merges files from both default and custom locations:
  287. - Files from data/{resource_type}/* (custom, higher priority)
  288. - Files from {resource_type}/* (default)
  289. - Duplicate names are deduplicated (custom takes precedence)
  290. Args:
  291. resource_type: Resource type ("bgm", "templates", "workflows")
  292. subdir: Optional subdirectory (e.g., "1080x1920" for templates)
  293. Returns:
  294. Sorted list of filenames (deduplicated, custom overrides default)
  295. Examples:
  296. >>> list_resource_files("bgm")
  297. # Returns: ["custom.mp3", "default.mp3", "happy.mp3"]
  298. # (merged from bgm/ and data/bgm/)
  299. >>> list_resource_files("templates", "1080x1920")
  300. # Returns: ["custom.html", "default.html", "modern.html"]
  301. # (merged from templates/1080x1920/ and data/templates/1080x1920/)
  302. """
  303. files = {} # Use dict to track source priority: {filename: path}
  304. # Build directory paths
  305. default_dir = Path(get_root_path(resource_type, subdir)) if subdir else Path(get_root_path(resource_type))
  306. custom_dir = Path(get_data_path(resource_type, subdir)) if subdir else Path(get_data_path(resource_type))
  307. # Scan default directory first (lower priority)
  308. if default_dir.exists() and default_dir.is_dir():
  309. for item in default_dir.iterdir():
  310. if item.is_file():
  311. files[item.name] = str(item)
  312. # Scan custom directory (higher priority, overwrites)
  313. if custom_dir.exists() and custom_dir.is_dir():
  314. for item in custom_dir.iterdir():
  315. if item.is_file():
  316. files[item.name] = str(item) # Overwrite if exists
  317. return sorted(files.keys())
  318. def list_resource_dirs(
  319. resource_type: Literal["bgm", "templates", "workflows"]
  320. ) -> list[str]:
  321. """
  322. List subdirectories in resource directory
  323. Merges directories from both default and custom locations.
  324. Args:
  325. resource_type: Resource type ("bgm", "templates", "workflows")
  326. Returns:
  327. Sorted list of directory names (deduplicated)
  328. Examples:
  329. >>> list_resource_dirs("templates")
  330. # Returns: ["1080x1080", "1080x1920", "1920x1080"]
  331. >>> list_resource_dirs("workflows")
  332. # Returns: ["runninghub", "selfhost"]
  333. """
  334. dirs = set()
  335. # Build directory paths
  336. default_dir = Path(get_root_path(resource_type))
  337. custom_dir = Path(get_data_path(resource_type))
  338. # Scan default directory
  339. if default_dir.exists() and default_dir.is_dir():
  340. for item in default_dir.iterdir():
  341. if item.is_dir():
  342. dirs.add(item.name)
  343. # Scan custom directory
  344. if custom_dir.exists() and custom_dir.is_dir():
  345. for item in custom_dir.iterdir():
  346. if item.is_dir():
  347. dirs.add(item.name)
  348. return sorted(dirs)
  349. def resource_exists(resource_type: Literal["bgm", "templates", "workflows"], *paths: str) -> bool:
  350. """
  351. Check if resource file exists (in custom or default location)
  352. Args:
  353. resource_type: Resource type ("bgm", "templates", "workflows")
  354. *paths: Path components relative to resource directory
  355. Returns:
  356. True if exists in either location, False otherwise
  357. Examples:
  358. >>> resource_exists("bgm", "happy.mp3")
  359. True
  360. >>> resource_exists("templates", "1080x1920", "default.html")
  361. True
  362. """
  363. custom_path = get_data_path(resource_type, *paths)
  364. default_path = get_root_path(resource_type, *paths)
  365. return os.path.exists(custom_path) or os.path.exists(default_path)