bilisum 1.13.2 → 1.13.3

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 (125) hide show
  1. package/README.md +4 -4
  2. package/bin/bilisum.js +157 -76
  3. package/package.json +7 -2
  4. package/runtime/VERSION +1 -0
  5. package/runtime/apps/service/pyproject.toml +29 -0
  6. package/runtime/apps/service/src/video_sum_service/__init__.py +1 -0
  7. package/runtime/apps/service/src/video_sum_service/__main__.py +5 -0
  8. package/runtime/apps/service/src/video_sum_service/app.py +480 -0
  9. package/runtime/apps/service/src/video_sum_service/context.py +24 -0
  10. package/runtime/apps/service/src/video_sum_service/integrations.py +264 -0
  11. package/runtime/apps/service/src/video_sum_service/knowledge/__init__.py +5 -0
  12. package/runtime/apps/service/src/video_sum_service/knowledge/index_service.py +408 -0
  13. package/runtime/apps/service/src/video_sum_service/knowledge/local_llm.py +319 -0
  14. package/runtime/apps/service/src/video_sum_service/knowledge/rag_service.py +492 -0
  15. package/runtime/apps/service/src/video_sum_service/knowledge/tag_service.py +242 -0
  16. package/runtime/apps/service/src/video_sum_service/main.py +31 -0
  17. package/runtime/apps/service/src/video_sum_service/repository.py +942 -0
  18. package/runtime/apps/service/src/video_sum_service/routers/__init__.py +1 -0
  19. package/runtime/apps/service/src/video_sum_service/routers/knowledge.py +272 -0
  20. package/runtime/apps/service/src/video_sum_service/routers/system.py +280 -0
  21. package/runtime/apps/service/src/video_sum_service/routers/tasks.py +287 -0
  22. package/runtime/apps/service/src/video_sum_service/routers/videos.py +766 -0
  23. package/runtime/apps/service/src/video_sum_service/runtime_support.py +1007 -0
  24. package/runtime/apps/service/src/video_sum_service/schemas.py +418 -0
  25. package/runtime/apps/service/src/video_sum_service/settings_manager.py +130 -0
  26. package/runtime/apps/service/src/video_sum_service/task_artifacts.py +102 -0
  27. package/runtime/apps/service/src/video_sum_service/task_exports.py +108 -0
  28. package/runtime/apps/service/src/video_sum_service/transcribe_worker.py +9 -0
  29. package/runtime/apps/service/src/video_sum_service/video_assets.py +623 -0
  30. package/runtime/apps/service/src/video_sum_service/worker.py +381 -0
  31. package/runtime/apps/web/static/apple-touch-icon.png +0 -0
  32. package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  33. package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  34. package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  35. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  36. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  37. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  38. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  39. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  40. package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  41. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  42. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  43. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  44. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  45. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  46. package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  47. package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  48. package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  49. package/runtime/apps/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  50. package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  51. package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  52. package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  53. package/runtime/apps/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  54. package/runtime/apps/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  55. package/runtime/apps/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  56. package/runtime/apps/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  57. package/runtime/apps/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  58. package/runtime/apps/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  59. package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  60. package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  61. package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  62. package/runtime/apps/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  63. package/runtime/apps/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  64. package/runtime/apps/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  65. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  66. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  67. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  68. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  69. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  70. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  71. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  72. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  73. package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  74. package/runtime/apps/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  75. package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  76. package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  77. package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  78. package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  79. package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  80. package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  81. package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  82. package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  83. package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  84. package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  85. package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  86. package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  87. package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  88. package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  89. package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  90. package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  91. package/runtime/apps/web/static/assets/icons/icon-180.png +0 -0
  92. package/runtime/apps/web/static/assets/icons/icon-512.png +0 -0
  93. package/runtime/apps/web/static/assets/icons/icon.svg +17 -0
  94. package/runtime/apps/web/static/assets/index-Cj5PVjQg.js +388 -0
  95. package/runtime/apps/web/static/assets/index-DU2t4_7s.css +1 -0
  96. package/runtime/apps/web/static/favicon-32x32.png +0 -0
  97. package/runtime/apps/web/static/favicon.ico +0 -0
  98. package/runtime/apps/web/static/favicon.svg +17 -0
  99. package/runtime/apps/web/static/index.html +19 -0
  100. package/runtime/apps/web/static/js/api.js +123 -0
  101. package/runtime/apps/web/static/js/main.js +932 -0
  102. package/runtime/apps/web/static/js/state.js +28 -0
  103. package/runtime/apps/web/static/js/utils.js +81 -0
  104. package/runtime/apps/web/static/js/views/home.js +604 -0
  105. package/runtime/apps/web/static/js/views/settings.js +477 -0
  106. package/runtime/apps/web/static/styles.css +2206 -0
  107. package/runtime/packages/core/pyproject.toml +18 -0
  108. package/runtime/packages/core/src/video_sum_core/__init__.py +1 -0
  109. package/runtime/packages/core/src/video_sum_core/errors.py +22 -0
  110. package/runtime/packages/core/src/video_sum_core/markdown_exports.py +154 -0
  111. package/runtime/packages/core/src/video_sum_core/models/__init__.py +1 -0
  112. package/runtime/packages/core/src/video_sum_core/models/tasks.py +70 -0
  113. package/runtime/packages/core/src/video_sum_core/pipeline/__init__.py +1 -0
  114. package/runtime/packages/core/src/video_sum_core/pipeline/base.py +46 -0
  115. package/runtime/packages/core/src/video_sum_core/pipeline/real.py +3467 -0
  116. package/runtime/packages/core/src/video_sum_core/transcribe_subprocess.py +173 -0
  117. package/runtime/packages/core/src/video_sum_core/utils.py +127 -0
  118. package/runtime/packages/infra/pyproject.toml +16 -0
  119. package/runtime/packages/infra/src/video_sum_infra/__init__.py +1 -0
  120. package/runtime/packages/infra/src/video_sum_infra/app.py +39 -0
  121. package/runtime/packages/infra/src/video_sum_infra/config.py +393 -0
  122. package/runtime/packages/infra/src/video_sum_infra/db.py +34 -0
  123. package/runtime/packages/infra/src/video_sum_infra/logging.py +54 -0
  124. package/runtime/packages/infra/src/video_sum_infra/paths.py +6 -0
  125. package/runtime/packages/infra/src/video_sum_infra/runtime.py +485 -0
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import logging
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ logger = logging.getLogger("video_sum_core.transcribe_subprocess")
12
+
13
+
14
+ def configure_logging() -> None:
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s %(levelname)s %(name)s | %(message)s",
18
+ )
19
+
20
+
21
+ def write_progress(progress_path: Path, payload: dict[str, object]) -> None:
22
+ with progress_path.open("a", encoding="utf-8") as handle:
23
+ handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
24
+ handle.flush()
25
+
26
+
27
+ def configure_runtime_library_dirs() -> None:
28
+ raw_paths = os.environ.get("VIDEO_SUM_DLL_PATHS", "")
29
+ if not raw_paths:
30
+ return
31
+ dll_paths: list[str] = []
32
+ for entry in raw_paths.split(os.pathsep):
33
+ path = entry.strip()
34
+ if path and path not in dll_paths and Path(path).exists():
35
+ dll_paths.append(path)
36
+
37
+ if not dll_paths:
38
+ return
39
+
40
+ os.environ["PATH"] = os.pathsep.join([*dll_paths, os.environ.get("PATH", "")])
41
+ add_dll_directory = getattr(os, "add_dll_directory", None)
42
+ if add_dll_directory is None:
43
+ return
44
+ for path in dll_paths:
45
+ try:
46
+ add_dll_directory(path)
47
+ except OSError:
48
+ logger.debug("skip dll directory path=%s", path, exc_info=True)
49
+
50
+
51
+ def transcribe(
52
+ audio_path: Path,
53
+ model_name: str,
54
+ device: str,
55
+ compute_type: str,
56
+ progress_path: Path,
57
+ output_path: Path,
58
+ duration: float | None,
59
+ ) -> None:
60
+ configure_runtime_library_dirs()
61
+ from faster_whisper import WhisperModel
62
+
63
+ logger.info(
64
+ "child transcription start audio=%s model=%s device=%s compute_type=%s",
65
+ audio_path,
66
+ model_name,
67
+ device,
68
+ compute_type,
69
+ )
70
+ model = WhisperModel(model_name, device=device, compute_type=compute_type)
71
+ raw_segments, _info = model.transcribe(str(audio_path), language="zh", vad_filter=True)
72
+
73
+ segments: list[dict[str, object]] = []
74
+ transcript_lines: list[str] = []
75
+ last_reported_progress = 58
76
+
77
+ for segment in raw_segments:
78
+ item = {
79
+ "start": round(segment.start, 3),
80
+ "end": round(segment.end, 3),
81
+ "text": segment.text.strip(),
82
+ }
83
+ segments.append(item)
84
+ transcript_lines.append(f"[{format_timestamp(item['start'])}] {item['text']}")
85
+
86
+ if duration and duration > 0:
87
+ progress = min(82, 58 + int((float(segment.end) / float(duration)) * 24))
88
+ else:
89
+ progress = min(82, 58 + min(24, len(segments)))
90
+
91
+ if progress > last_reported_progress:
92
+ last_reported_progress = progress
93
+ write_progress(
94
+ progress_path,
95
+ {
96
+ "stage": "transcribing",
97
+ "progress": progress,
98
+ "message": f"正在转写,已识别 {len(segments)} 段",
99
+ "payload": {
100
+ "segment_count": len(segments),
101
+ "current_time": round(float(segment.end), 3),
102
+ },
103
+ },
104
+ )
105
+
106
+ transcript = "\n".join(transcript_lines)
107
+ if not transcript.strip():
108
+ raise RuntimeError("Transcription produced empty output.")
109
+
110
+ write_progress(
111
+ progress_path,
112
+ {
113
+ "stage": "transcribing",
114
+ "progress": 84,
115
+ "message": f"转写完成,共识别 {len(segments)} 段",
116
+ "payload": {"segment_count": len(segments)},
117
+ },
118
+ )
119
+ output_path.write_text(
120
+ json.dumps({"transcript": transcript, "segments": segments}, ensure_ascii=False),
121
+ encoding="utf-8",
122
+ )
123
+ logger.info(
124
+ "child transcription finish audio=%s segments=%d transcript_chars=%d",
125
+ audio_path,
126
+ len(segments),
127
+ len(transcript),
128
+ )
129
+
130
+
131
+ def format_timestamp(seconds: float) -> str:
132
+ total = max(0, int(seconds))
133
+ hours = total // 3600
134
+ minutes = (total % 3600) // 60
135
+ secs = total % 60
136
+ if hours:
137
+ return f"{hours:02d}:{minutes:02d}:{secs:02d}"
138
+ return f"{minutes:02d}:{secs:02d}"
139
+
140
+
141
+ def parse_args() -> argparse.Namespace:
142
+ parser = argparse.ArgumentParser(description="Run faster-whisper transcription in an isolated subprocess.")
143
+ parser.add_argument("--audio-path", required=True)
144
+ parser.add_argument("--model", required=True)
145
+ parser.add_argument("--device", required=True)
146
+ parser.add_argument("--compute-type", required=True)
147
+ parser.add_argument("--progress-path", required=True)
148
+ parser.add_argument("--output-path", required=True)
149
+ parser.add_argument("--duration", type=float)
150
+ return parser.parse_args()
151
+
152
+
153
+ def main() -> int:
154
+ configure_logging()
155
+ args = parse_args()
156
+ try:
157
+ transcribe(
158
+ audio_path=Path(args.audio_path),
159
+ model_name=args.model,
160
+ device=args.device,
161
+ compute_type=args.compute_type,
162
+ progress_path=Path(args.progress_path),
163
+ output_path=Path(args.output_path),
164
+ duration=args.duration,
165
+ )
166
+ except Exception:
167
+ logger.exception("child transcription failed")
168
+ return 1
169
+ return 0
170
+
171
+
172
+ if __name__ == "__main__":
173
+ raise SystemExit(main())
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from urllib.parse import parse_qs, urlencode, urlparse
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class NormalizedVideoUrl:
11
+ normalized_url: str
12
+ canonical_id: str
13
+ platform: str = "unknown"
14
+ page_number: int | None = None
15
+
16
+ def __iter__(self):
17
+ # Backward-compatible tuple unpacking for existing call sites.
18
+ yield self.normalized_url
19
+ yield self.canonical_id
20
+
21
+
22
+ def normalize_video_url(value: str) -> NormalizedVideoUrl:
23
+ raw = (value or "").strip()
24
+ if not raw:
25
+ return NormalizedVideoUrl(normalized_url="", canonical_id="")
26
+
27
+ match = re.search(r"(BV[0-9A-Za-z]+)", raw, flags=re.IGNORECASE)
28
+ if match:
29
+ bvid = match.group(1)
30
+ normalized_url = f"https://www.bilibili.com/video/{bvid}"
31
+ page = extract_bilibili_page(raw)
32
+ if page is not None:
33
+ normalized_url = f"{normalized_url}?{urlencode({'p': page})}"
34
+ return NormalizedVideoUrl(
35
+ normalized_url=normalized_url,
36
+ canonical_id=bvid,
37
+ platform="bilibili",
38
+ page_number=page,
39
+ )
40
+
41
+ youtube_id = extract_youtube_video_id(raw)
42
+ if youtube_id:
43
+ return NormalizedVideoUrl(
44
+ normalized_url=f"https://www.youtube.com/watch?v={youtube_id}",
45
+ canonical_id=youtube_id,
46
+ platform="youtube",
47
+ )
48
+
49
+ return NormalizedVideoUrl(normalized_url=raw, canonical_id="")
50
+
51
+
52
+ def extract_bilibili_page(value: str) -> int | None:
53
+ raw = (value or "").strip()
54
+ if not raw:
55
+ return None
56
+
57
+ if not re.search(r"(BV[0-9A-Za-z]+|bilibili\.com|b23\.tv)", raw, flags=re.IGNORECASE):
58
+ return None
59
+
60
+ try:
61
+ parsed = urlparse(raw)
62
+ query = parse_qs(parsed.query)
63
+ page_value = query.get("p", [None])[0]
64
+ except ValueError:
65
+ page_value = None
66
+
67
+ if page_value is None:
68
+ match = re.search(r"[?&]p=(\d+)", raw, flags=re.IGNORECASE)
69
+ page_value = match.group(1) if match else None
70
+
71
+ if page_value is None:
72
+ return None
73
+
74
+ try:
75
+ page = int(str(page_value))
76
+ except (TypeError, ValueError):
77
+ return None
78
+ return page if page > 0 else None
79
+
80
+
81
+ def extract_youtube_video_id(value: str) -> str | None:
82
+ raw = (value or "").strip()
83
+ if not raw:
84
+ return None
85
+
86
+ try:
87
+ parsed = urlparse(raw)
88
+ except ValueError:
89
+ parsed = None
90
+
91
+ if parsed is not None:
92
+ host = parsed.netloc.lower()
93
+ path = parsed.path or ""
94
+
95
+ if host.endswith("youtu.be"):
96
+ candidate = path.strip("/").split("/", 1)[0]
97
+ return candidate or None
98
+
99
+ if "youtube.com" in host:
100
+ if path == "/watch":
101
+ candidate = parse_qs(parsed.query).get("v", [None])[0]
102
+ return str(candidate).strip() or None
103
+ if path.startswith("/shorts/"):
104
+ candidate = path.split("/", 3)[2] if len(path.split("/")) > 2 else ""
105
+ return candidate.strip() or None
106
+
107
+ return None
108
+
109
+
110
+ def sanitize_filename(value: str) -> str:
111
+ sanitized = re.sub(r"[\\/:*?\"<>|]+", "_", value).strip()
112
+ return sanitized[:120] or "video_summary"
113
+
114
+
115
+ def ensure_directory(path: Path) -> Path:
116
+ path.mkdir(parents=True, exist_ok=True)
117
+ return path
118
+
119
+
120
+ def format_timestamp(seconds: float) -> str:
121
+ total = max(0, int(seconds))
122
+ hours = total // 3600
123
+ minutes = (total % 3600) // 60
124
+ remaining = total % 60
125
+ if hours:
126
+ return f"{hours:02d}:{minutes:02d}:{remaining:02d}"
127
+ return f"{minutes:02d}:{remaining:02d}"
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "video-sum-infra"
7
+ version = "1.13.3"
8
+ description = "Infrastructure layer for the local video summarizer."
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "pydantic>=2.10,<3.0",
12
+ "pydantic-settings>=2.7,<3.0",
13
+ ]
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/video_sum_infra"]
@@ -0,0 +1 @@
1
+ """Infrastructure package for local video summarizer."""
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass
2
+ from importlib import metadata
3
+ from pathlib import Path
4
+ import sys
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class AppInfo:
9
+ name: str
10
+ version: str
11
+
12
+ @classmethod
13
+ def load(cls) -> "AppInfo":
14
+ return cls(name="BiliSum", version=_resolve_version())
15
+
16
+
17
+ def _resolve_version() -> str:
18
+ version_file = Path(__file__).resolve().parents[4] / "VERSION"
19
+
20
+ # In local development, prefer the workspace VERSION file so the UI reflects
21
+ # the checked-out repo version immediately without reinstalling packages.
22
+ if version_file.exists() and not _is_running_from_installed_dist():
23
+ return version_file.read_text(encoding="utf-8").strip()
24
+
25
+ for distribution_name in ("video-sum-service", "video-sum-infra"):
26
+ try:
27
+ return metadata.version(distribution_name)
28
+ except metadata.PackageNotFoundError:
29
+ continue
30
+
31
+ if version_file.exists():
32
+ return version_file.read_text(encoding="utf-8").strip()
33
+ return "0.0.0"
34
+
35
+
36
+ def _is_running_from_installed_dist() -> bool:
37
+ base_prefix = Path(getattr(sys, "base_prefix", sys.prefix)).resolve()
38
+ current_file = Path(__file__).resolve()
39
+ return base_prefix in current_file.parents