lightning-pose-app 1.8.1a3__py3-none-any.whl → 1.8.1a5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. lightning_pose_app-1.8.1a5.dist-info/METADATA +23 -0
  2. lightning_pose_app-1.8.1a5.dist-info/RECORD +29 -0
  3. litpose_app/config.py +22 -0
  4. litpose_app/deps.py +77 -0
  5. litpose_app/main.py +161 -300
  6. litpose_app/ngdist/ng_app/3rdpartylicenses.txt +11 -11
  7. litpose_app/ngdist/ng_app/index.html +3 -2
  8. litpose_app/ngdist/ng_app/{main-LJHMLKBL.js → main-6XYUWDGZ.js} +370 -203
  9. litpose_app/ngdist/ng_app/main-6XYUWDGZ.js.map +1 -0
  10. litpose_app/ngdist/ng_app/{styles-4V6RXJMC.css → styles-GMK322VW.css} +32 -1
  11. litpose_app/ngdist/ng_app/styles-GMK322VW.css.map +7 -0
  12. litpose_app/{run_ffprobe.py → routes/ffprobe.py} +164 -132
  13. litpose_app/{super_rglob.py → routes/files.py} +108 -48
  14. litpose_app/routes/project.py +72 -0
  15. litpose_app/routes/transcode.py +131 -0
  16. litpose_app/tasks/__init__.py +0 -0
  17. litpose_app/tasks/management.py +20 -0
  18. litpose_app/tasks/transcode_fine.py +7 -0
  19. litpose_app/transcode_fine.py +173 -0
  20. lightning_pose_app-1.8.1a3.dist-info/METADATA +0 -15
  21. lightning_pose_app-1.8.1a3.dist-info/RECORD +0 -21
  22. litpose_app/ngdist/ng_app/main-LJHMLKBL.js.map +0 -1
  23. litpose_app/ngdist/ng_app/styles-4V6RXJMC.css.map +0 -7
  24. {lightning_pose_app-1.8.1a3.dist-info → lightning_pose_app-1.8.1a5.dist-info}/WHEEL +0 -0
  25. /litpose_app/ngdist/ng_app/{app.component-UHVEDPZR.css.map → app.component-UAQUAGNZ.css.map} +0 -0
  26. /litpose_app/ngdist/ng_app/{project-settings.component-5IRK7U7U.css.map → project-settings.component-HKHIVUJR.css.map} +0 -0
  27. /litpose_app/ngdist/ng_app/{video-tile.component-XSYKMARQ.css.map → video-tile.component-RDL4BSJ4.css.map} +0 -0
  28. /litpose_app/ngdist/ng_app/{viewer-page.component-MRTIUFL2.css.map → viewer-page.component-KDHT6XH5.css.map} +0 -0
@@ -1,48 +1,108 @@
1
- import datetime
2
-
3
- from wcmatch import pathlib as w
4
-
5
-
6
- def super_rglob(base_path, pattern=None, no_dirs=False, stat=False):
7
- """
8
- Needs to be performant when searching over large model directory.
9
- Uses wcmatch to exclude directories with extra calls to Path.is_dir.
10
- wcmatch includes features that may be helpful down the line.
11
- """
12
- if pattern is None:
13
- pattern = "**/*"
14
- flags = w.GLOBSTAR
15
- if no_dirs:
16
- flags |= w.NODIR
17
- results = w.Path(base_path).glob(
18
- pattern,
19
- flags=flags,
20
- )
21
- result_dicts = []
22
- for r in results:
23
- stat_info = r.stat() if stat else None
24
- is_dir = False if no_dirs else r.is_dir() if stat else None
25
- if no_dirs and is_dir:
26
- continue
27
- entry_relative_path = r.relative_to(base_path)
28
- d = {
29
- "path": entry_relative_path,
30
- "type": "dir" if is_dir else "file" if is_dir == False else None,
31
- "size": stat_info.st_size if stat_info else None,
32
- # Note: st_birthtime is more reliable for creation time on some systems
33
- "cTime": (
34
- datetime.datetime.fromtimestamp(
35
- getattr(stat_info, "st_birthtime", stat_info.st_ctime)
36
- ).isoformat()
37
- if stat_info
38
- else None
39
- ),
40
- "mTime": (
41
- datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
42
- if stat_info
43
- else None
44
- ),
45
- }
46
-
47
- result_dicts.append(d)
48
- return result_dicts
1
+ from pathlib import Path
2
+
3
+ from fastapi import APIRouter, HTTPException, status
4
+ from pydantic import BaseModel
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ class RGlobRequest(BaseModel):
10
+ baseDir: Path
11
+ pattern: str
12
+ noDirs: bool = False
13
+ stat: bool = False
14
+
15
+
16
+ class RGlobResponseEntry(BaseModel):
17
+ path: Path
18
+
19
+ # Present only if request had stat=True or noDirs=True
20
+ type: str | None
21
+
22
+ # Present only if request had stat=True
23
+
24
+ size: int | None
25
+ # Creation timestamp, ISO format.
26
+ cTime: str | None
27
+ # Modified timestamp, ISO format.
28
+ mTime: str | None
29
+
30
+
31
+ class RGlobResponse(BaseModel):
32
+ entries: list[RGlobResponseEntry]
33
+ relativeTo: Path # this is going to be the same base_dir that was in the request.
34
+
35
+
36
+ @router.post("/app/v0/rpc/rglob")
37
+ def rglob(request: RGlobRequest) -> RGlobResponse:
38
+ # Prevent secrets like /etc/passwd and ~/.ssh/ from being leaked.
39
+ if not (request.pattern.endswith(".csv") or request.pattern.endswith(".mp4")):
40
+ raise HTTPException(
41
+ status_code=status.HTTP_403_FORBIDDEN,
42
+ detail="Only csv and mp4 files are supported.",
43
+ )
44
+
45
+ response = RGlobResponse(entries=[], relativeTo=request.baseDir)
46
+
47
+ results = super_rglob(
48
+ str(request.baseDir),
49
+ pattern=request.pattern,
50
+ no_dirs=request.noDirs,
51
+ stat=request.stat,
52
+ )
53
+ for r in results:
54
+ # Convert dict to pydantic model
55
+ converted = RGlobResponseEntry.model_validate(r)
56
+ response.entries.append(converted)
57
+
58
+ return response
59
+
60
+
61
+ import datetime
62
+
63
+ from wcmatch import pathlib as w
64
+
65
+
66
+ def super_rglob(base_path, pattern=None, no_dirs=False, stat=False):
67
+ """
68
+ Needs to be performant when searching over large model directory.
69
+ Uses wcmatch to exclude directories with extra calls to Path.is_dir.
70
+ wcmatch includes features that may be helpful down the line.
71
+ """
72
+ if pattern is None:
73
+ pattern = "**/*"
74
+ flags = w.GLOBSTAR
75
+ if no_dirs:
76
+ flags |= w.NODIR
77
+ results = w.Path(base_path).glob(
78
+ pattern,
79
+ flags=flags,
80
+ )
81
+ result_dicts = []
82
+ for r in results:
83
+ stat_info = r.stat() if stat else None
84
+ is_dir = False if no_dirs else r.is_dir() if stat else None
85
+ if no_dirs and is_dir:
86
+ continue
87
+ entry_relative_path = r.relative_to(base_path)
88
+ d = {
89
+ "path": entry_relative_path,
90
+ "type": "dir" if is_dir else "file" if is_dir == False else None,
91
+ "size": stat_info.st_size if stat_info else None,
92
+ # Note: st_birthtime is more reliable for creation time on some systems
93
+ "cTime": (
94
+ datetime.datetime.fromtimestamp(
95
+ getattr(stat_info, "st_birthtime", stat_info.st_ctime)
96
+ ).isoformat()
97
+ if stat_info
98
+ else None
99
+ ),
100
+ "mTime": (
101
+ datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
102
+ if stat_info
103
+ else None
104
+ ),
105
+ }
106
+
107
+ result_dicts.append(d)
108
+ return result_dicts
@@ -0,0 +1,72 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import tomli
5
+ import tomli_w
6
+ from fastapi import APIRouter, Depends
7
+ from pydantic import BaseModel
8
+
9
+ from litpose_app import deps
10
+ from litpose_app.config import Config
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ class ProjectInfo(BaseModel):
18
+ """Class to hold information about the project"""
19
+
20
+ data_dir: Path | None = None
21
+ model_dir: Path | None = None
22
+ views: list[str] | None = None
23
+
24
+
25
+ class GetProjectInfoResponse(BaseModel):
26
+ projectInfo: ProjectInfo | None # None if project info not yet initialized
27
+
28
+
29
+ class SetProjectInfoRequest(BaseModel):
30
+ projectInfo: ProjectInfo
31
+
32
+
33
+ @router.post("/app/v0/rpc/getProjectInfo")
34
+ def get_project_info(
35
+ project_info: ProjectInfo = Depends(deps.project_info),
36
+ ) -> GetProjectInfoResponse:
37
+ return GetProjectInfoResponse(projectInfo=project_info)
38
+
39
+
40
+ @router.post("/app/v0/rpc/setProjectInfo")
41
+ def set_project_info(
42
+ request: SetProjectInfoRequest, config: Config = Depends(deps.config)
43
+ ) -> None:
44
+ try:
45
+ config.PROJECT_INFO_TOML_PATH.parent.mkdir(parents=True, exist_ok=True)
46
+
47
+ project_data_dict = request.projectInfo.model_dump(
48
+ mode="json", exclude_none=True
49
+ )
50
+ try:
51
+ with open(config.PROJECT_INFO_TOML_PATH, "rb") as f:
52
+ existing_project_data = tomli.load(f)
53
+ except FileNotFoundError:
54
+ existing_project_data = {}
55
+
56
+ existing_project_data.update(project_data_dict)
57
+
58
+ with open(config.PROJECT_INFO_TOML_PATH, "wb") as f:
59
+ tomli_w.dump(existing_project_data, f)
60
+
61
+ return None
62
+
63
+ except IOError as e:
64
+ error_message = f"Failed to write project information to file: {str(e)}"
65
+ print(error_message)
66
+ raise e
67
+ except Exception as e:
68
+ error_message = (
69
+ f"An unexpected error occurred while saving project info: {str(e)}"
70
+ )
71
+ print(error_message)
72
+ raise e
@@ -0,0 +1,131 @@
1
+ import asyncio
2
+ import json
3
+ from typing import AsyncGenerator
4
+
5
+ import reactivex
6
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
7
+ from fastapi import APIRouter, Depends, Request
8
+ from fastapi.responses import StreamingResponse
9
+ from reactivex import Observable
10
+ import reactivex.operators
11
+
12
+ from .files import super_rglob
13
+ from .project import ProjectInfo
14
+ from .. import deps
15
+ from ..tasks import transcode_fine
16
+ import logging
17
+
18
+ logger = logging.getLogger(__name__)
19
+ router = APIRouter()
20
+ from litpose_app.config import Config
21
+
22
+
23
+ @router.post("/app/v0/rpc/getFineVideoDir")
24
+ def get_fine_video_dir(config: Config = Depends(deps.config)):
25
+ return {"path": config.FINE_VIDEO_DIR}
26
+
27
+
28
+ @router.get("/app/v0/rpc/getFineVideoStatus")
29
+ async def get_fine_video_status(request: Request) -> StreamingResponse:
30
+ """
31
+ Returns number of pending transcode tasks.
32
+ """
33
+ subject: reactivex.Subject = request.app.state.num_active_transcode_tasks
34
+ wrapped = subject.pipe(reactivex.operators.map(lambda x: {"pending": x}))
35
+ return await sse_events(request, wrapped)
36
+
37
+
38
+ @router.post("/app/v0/rpc/enqueueAllNewFineVideos")
39
+ async def enqueue_all_new_fine_videos(
40
+ config: Config = Depends(deps.config),
41
+ project_info: ProjectInfo = Depends(deps.project_info),
42
+ scheduler: AsyncIOScheduler = Depends(deps.scheduler),
43
+ ):
44
+ # get all mp4 video files that are less than config.AUTO_TRANSCODE_VIDEO_SIZE_LIMIT_MB
45
+ base_path = project_info.data_dir
46
+ result = await asyncio.to_thread(
47
+ super_rglob, base_path, pattern="**/*.mp4", stat=True
48
+ )
49
+
50
+ # Filter videos by size limit
51
+ videos = [
52
+ base_path / entry["path"]
53
+ for entry in result
54
+ if entry["size"]
55
+ and entry["size"] < config.AUTO_TRANSCODE_VIDEO_SIZE_LIMIT_MB * 1000 * 1000
56
+ ]
57
+
58
+ # Create a transcode job per video.
59
+ # The id of the job is just the filename. We assume unique video filenames
60
+ # across the entire dataset.
61
+ for path in videos:
62
+ scheduler.add_job(
63
+ transcode_fine.transcode_video_task,
64
+ id=path.name,
65
+ args=[path, config.FINE_VIDEO_DIR / path.name],
66
+ executor="transcode_pool",
67
+ # executor="debug",
68
+ replace_existing=True,
69
+ misfire_grace_time=None,
70
+ )
71
+
72
+ return "ok"
73
+
74
+
75
+ async def sse_events(request: Request, source: Observable) -> StreamingResponse:
76
+ # An asyncio.Queue will act as a bridge between the synchronous world of
77
+ # the RxPy observable and the async world of the FastAPI response.
78
+ queue = asyncio.Queue()
79
+
80
+ # The scheduler ensures that the observable runs on the asyncio event loop,
81
+ # which is necessary for it to work correctly with FastAPI.
82
+ scheduler = AsyncIOScheduler()
83
+
84
+ async def event_generator() -> AsyncGenerator[str, None]:
85
+ """
86
+ This async generator function is the core of the SSE streaming.
87
+ It listens for items on the queue and yields them in the SSE format.
88
+ """
89
+ disposable = None
90
+ try:
91
+ # Subscribe to the observable.
92
+ # For each item emitted by the observable (`on_next`), we put it
93
+ # into our asyncio queue.
94
+ # If the observable completes (`on_completed`) or errors (`on_error`),
95
+ # we put a special sentinel value (None) in the queue to signal the end.
96
+ disposable = source.subscribe(
97
+ on_next=lambda item: queue.put_nowait(item),
98
+ on_error=lambda e: queue.put_nowait(None),
99
+ on_completed=lambda: queue.put_nowait(None),
100
+ scheduler=scheduler,
101
+ )
102
+
103
+ while True:
104
+ # Check if the client has disconnected.
105
+ if await request.is_disconnected():
106
+ logger.debug("Client disconnected.")
107
+ break
108
+
109
+ # Wait for an item to appear on the queue.
110
+ item = await queue.get()
111
+
112
+ # If the item is our sentinel value, it means the observable
113
+ # has finished, and we can stop streaming.
114
+ if item is None:
115
+ break
116
+
117
+ # Yield the data in the Server-Sent Event format.
118
+ # The format is "data: {your_message}\n\n"
119
+ yield f"data: {json.dumps(item)}\n\n"
120
+
121
+ finally:
122
+ # This block is crucial for cleanup. It runs when the client
123
+ # disconnects or the stream is otherwise closed.
124
+ # We dispose of the subscription to the observable, which stops
125
+ # the interval and prevents memory leaks.
126
+ if disposable:
127
+ disposable.dispose()
128
+ logger.debug("Observable subscription disposed.")
129
+ logger.debug("Event generator finished.")
130
+
131
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
File without changes
@@ -0,0 +1,20 @@
1
+ import reactivex
2
+ import reactivex.subject
3
+ from apscheduler import events as e
4
+ from apscheduler.schedulers.base import BaseScheduler
5
+ from fastapi import FastAPI
6
+
7
+
8
+ def setup_active_task_registry(app: FastAPI):
9
+ # Get APScheduler instance
10
+ scheduler: BaseScheduler = app.state.scheduler
11
+
12
+ subject = reactivex.subject.BehaviorSubject(len(scheduler.get_jobs()))
13
+
14
+ app.state.num_active_transcode_tasks = subject
15
+
16
+ def my_listener(event):
17
+ jobs = scheduler.get_jobs()
18
+ subject.on_next(len(jobs))
19
+
20
+ scheduler.add_listener(my_listener, e.EVENT_JOB_ADDED | e.EVENT_JOB_REMOVED)
@@ -0,0 +1,7 @@
1
+ from pathlib import Path
2
+
3
+ from litpose_app.transcode_fine import transcode_file
4
+
5
+
6
+ def transcode_video_task(input_file_path: Path, output_file_path: Path):
7
+ transcode_file(input_file_path, output_file_path)
@@ -0,0 +1,173 @@
1
+ import shutil
2
+ import subprocess
3
+ from multiprocessing import Pool, cpu_count
4
+ from pathlib import Path
5
+
6
+ # --- Configuration ---
7
+ TARGET_SUFFIX = "sec.mp4"
8
+ OUTPUT_SUFFIX_ADDITION = ".fine"
9
+ MAX_CONCURRENCY = 6
10
+ # FFmpeg options for transcoding:
11
+ # -g 1: Intra frame for every frame (Group of Pictures size 1)
12
+ # -c:v libx264: Use libx264 encoder
13
+ # -preset medium: A balance between encoding speed and compression.
14
+ # Options: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow.
15
+ # -crf 23: Constant Rate Factor. Lower values mean better quality and larger files (0-51, default 23).
16
+ # -c:a copy: Copy audio stream without re-encoding. If audio re-encoding is needed, change this.
17
+ # -y: Overwrite output files without asking.
18
+ FFMPEG_OPTIONS = [
19
+ "-c:v",
20
+ "libx264",
21
+ "-g",
22
+ "1",
23
+ "-preset",
24
+ "medium",
25
+ "-crf",
26
+ "23",
27
+ "-c:a",
28
+ "copy",
29
+ ]
30
+
31
+
32
+ def check_dependencies():
33
+ """Checks if ffmpeg and ffprobe are installed and in PATH."""
34
+ if shutil.which("ffmpeg") is None:
35
+ print("Error: ffmpeg is not installed or not found in PATH.")
36
+ return False
37
+ if shutil.which("ffprobe") is None:
38
+ print("Error: ffprobe is not installed or not found in PATH.")
39
+ return False
40
+ return True
41
+
42
+
43
+ def transcode_file(
44
+ input_file_path: Path,
45
+ output_file_path: Path,
46
+ ) -> tuple[bool, str, Path | None]:
47
+ """
48
+ Transcodes a single video file to have an intra frame for every frame.
49
+ The output file will be named by inserting ".fine" before the final ".mp4"
50
+ and placed in the specified output_dir.
51
+ Example: "video.sec.mp4" -> "video.sec.fine.mp4"
52
+ Returns a tuple: (success_status: bool, message: str, output_path: Path | None)
53
+ """
54
+ try:
55
+
56
+ if output_file_path.exists():
57
+ print(
58
+ f"Output file '{output_file_path.name}' already exists. Skipping transcoding."
59
+ )
60
+ return True, f"Skipped (exists): {output_file_path.name}", output_file_path
61
+
62
+ print(f"Processing: {input_file_path.name} -> {output_file_path.name}")
63
+ output_file_path.parent.mkdir(parents=True, exist_ok=True)
64
+ ffmpeg_cmd = [
65
+ "ffmpeg",
66
+ "-i",
67
+ str(input_file_path),
68
+ *FFMPEG_OPTIONS,
69
+ "-y", # Overwrite output without asking (though we check existence above)
70
+ str(output_file_path),
71
+ ]
72
+
73
+ process = subprocess.Popen(
74
+ ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
75
+ )
76
+
77
+ stdout, stderr = process.communicate()
78
+
79
+ if process.returncode == 0:
80
+ print(f"Successfully transcoded: {output_file_path.name}")
81
+ return True, f"Success: {output_file_path.name}", output_file_path
82
+ else:
83
+ print(f"Error transcoding '{input_file_path.name}':")
84
+ print(f"FFmpeg stdout:\n{stdout}")
85
+ print(f"FFmpeg stderr:\n{stderr}")
86
+ # Clean up partially created file on error
87
+ if output_file_path.exists():
88
+ try:
89
+ output_file_path.unlink()
90
+ except OSError as e:
91
+ print(
92
+ f"Could not remove partially created file '{output_file_path}': {e}"
93
+ )
94
+ return (
95
+ False,
96
+ f"Error: {input_file_path.name} - FFmpeg failed (code {process.returncode})",
97
+ None,
98
+ )
99
+
100
+ except Exception as e:
101
+ print(f"Error processing '{input_file_path.name}': {e}")
102
+ return False, f"Error: {input_file_path.name} - Exception: {e}", None
103
+
104
+
105
+ def main():
106
+ """
107
+ Main function to find and transcode videos.
108
+ """
109
+ if not check_dependencies():
110
+ return
111
+
112
+ script_dir = Path(__file__).parent # Process files in the script's directory
113
+ # To process files in the current working directory instead:
114
+ # current_dir = Path.cwd()
115
+
116
+ print(
117
+ f"Scanning for '*{TARGET_SUFFIX}' H.264 files in '{script_dir}' and its subdirectories..."
118
+ )
119
+
120
+ # Find all files ending with TARGET_SUFFIX recursively
121
+ files_to_check = list(script_dir.rglob(f"*{TARGET_SUFFIX}"))
122
+
123
+ if not files_to_check:
124
+ print(f"No files found ending with '{TARGET_SUFFIX}'.")
125
+ return
126
+
127
+ print(f"Found {len(files_to_check)} potential files. Checking H.264 codec...")
128
+
129
+ valid_files_to_transcode = []
130
+ for f_path in files_to_check:
131
+ # Ensure it's not an already processed file
132
+ if OUTPUT_SUFFIX_ADDITION + ".mp4" in f_path.name:
133
+ continue
134
+ valid_files_to_transcode.append(f_path)
135
+
136
+ if not valid_files_to_transcode:
137
+ print("No H.264 files matching the criteria need transcoding.")
138
+ return
139
+
140
+ print(f"\nFound {len(valid_files_to_transcode)} H.264 files to transcode:")
141
+ for f in valid_files_to_transcode:
142
+ print(f" - {f.name}")
143
+
144
+ # Determine number of processes
145
+ num_processes = min(MAX_CONCURRENCY, cpu_count(), len(valid_files_to_transcode))
146
+ print(f"\nStarting transcoding with up to {num_processes} parallel processes...\n")
147
+
148
+ output_file_paths = []
149
+ for f in valid_files_to_transcode:
150
+ base_name = f.name[: -len(TARGET_SUFFIX)]
151
+ output_file_name = f"{base_name}{TARGET_SUFFIX.replace('.mp4', '')}{OUTPUT_SUFFIX_ADDITION}.mp4"
152
+ output_file_path = f.parent / output_file_name
153
+ output_file_paths.append(output_file_path)
154
+ # In this main function, output_dir is still the parent of the input file
155
+ # For RPC, we will specify FINE_VIDEO_DIR as output_dir
156
+ with Pool(processes=num_processes) as pool:
157
+ # A dummy output_dir for the script's main function; not used by RPC.
158
+ # It ensures compatibility if this script were run standalone.
159
+ # In the RPC, we'll pass FINE_VIDEO_DIR explicitly.
160
+ results = pool.starmap(
161
+ transcode_file,
162
+ [(f, of) for f, of in zip(valid_files_to_transcode, output_file_paths)],
163
+ )
164
+
165
+ print("\n--- Transcoding Summary ---")
166
+ for result in results:
167
+ print(result)
168
+ print("--------------------------")
169
+ print("All tasks completed.")
170
+
171
+
172
+ if __name__ == "__main__":
173
+ main()
@@ -1,15 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: lightning-pose-app
3
- Version: 1.8.1a3
4
- Summary:
5
- Requires-Python: >=3.10
6
- Classifier: Programming Language :: Python :: 3
7
- Classifier: Programming Language :: Python :: 3.10
8
- Classifier: Programming Language :: Python :: 3.11
9
- Classifier: Programming Language :: Python :: 3.12
10
- Classifier: Programming Language :: Python :: 3.13
11
- Requires-Dist: fastapi
12
- Requires-Dist: tomli
13
- Requires-Dist: tomli_w
14
- Requires-Dist: uvicorn[standard]
15
- Requires-Dist: wcmatch
@@ -1,21 +0,0 @@
1
- litpose_app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- litpose_app/main.py,sha256=ZjD5i_zUVbSxsHPEn6J2TEtPTCEAdtqP5_-DndakixY,10037
3
- litpose_app/ngdist/ng_app/3rdpartylicenses.txt,sha256=k34Q8jZikvFWsgFXPlQxFJVcqMXDEiKKzC_VIEK8jNs,25366
4
- litpose_app/ngdist/ng_app/app.component-UHVEDPZR.css.map,sha256=e6lXWzmK3xppKK3tXHUccK1yGZqd1kzyTpDH0F1nC2g,344
5
- litpose_app/ngdist/ng_app/error-dialog.component-HYLQSJEP.css.map,sha256=zJuF-LfB994Y1IrnIz38mariDFb8yucffbWPXgHGbvw,355
6
- litpose_app/ngdist/ng_app/favicon.ico,sha256=QtbXVfx3HI-WqWh_kkBIQyYzJsDmLw_3Y4_UN7pBepE,15406
7
- litpose_app/ngdist/ng_app/index.html,sha256=ATacgBvjlF3gLRzFCtDnS3shRzTOp8Rzu4rKfltBDOQ,22954
8
- litpose_app/ngdist/ng_app/main-LJHMLKBL.js,sha256=nXrPvnVQwhHQJdHMsF-41nu5Mu7K6WARxWSei2g0y58,2722143
9
- litpose_app/ngdist/ng_app/main-LJHMLKBL.js.map,sha256=ltcuu1XsSXJv0p_KjbUSPBKi9zyAy4sVQXmJxZWNkos,5557811
10
- litpose_app/ngdist/ng_app/prerendered-routes.json,sha256=p53cyKEVGQ6cGUee02kUdBp9HbdPChFTUp78gHJVBf4,18
11
- litpose_app/ngdist/ng_app/project-settings.component-5IRK7U7U.css.map,sha256=v5tyba9p8ec3ZbHYyyUGTEFdEAsqT0l7JqtGRGjki6w,371
12
- litpose_app/ngdist/ng_app/styles-4V6RXJMC.css,sha256=mZ4r-BPkV1KTv3FvvjCJWuUygILiOzl8Jzjesyo0P8U,69300
13
- litpose_app/ngdist/ng_app/styles-4V6RXJMC.css.map,sha256=Fm7m4Uakz0HLL1DMEyfCAAXoFia46_3Eab27wowU9tU,74911
14
- litpose_app/ngdist/ng_app/video-player-controls.component-C4JZHYJ2.css.map,sha256=vX-dgeDCUCPLiea4Qy9O_EBm6IzzwB7R_uSBa0qU5Go,771
15
- litpose_app/ngdist/ng_app/video-tile.component-XSYKMARQ.css.map,sha256=_pZ7FxqOAu535JfrRv1TSKgRpDyQvn9Q0U39KHyJ980,332
16
- litpose_app/ngdist/ng_app/viewer-page.component-MRTIUFL2.css.map,sha256=Uf1FgoCiF_qJpD4Ggk34Dq7kM73Q7i7NT6h3m30tbaY,211
17
- litpose_app/run_ffprobe.py,sha256=7acBcSJ-3t8N_6HZ2KrTrMkivPsxKlGuYzI6yNhaXEs,4975
18
- litpose_app/super_rglob.py,sha256=kgSIra2xznEHhi9kBUzO0SpBb6PkUJCct0WSS33bRmM,1619
19
- lightning_pose_app-1.8.1a3.dist-info/METADATA,sha256=2JCRkaIImYE4r6Y-UcBvGGe7tkRfXlGH2X2PlrMih7A,473
20
- lightning_pose_app-1.8.1a3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
21
- lightning_pose_app-1.8.1a3.dist-info/RECORD,,