xscribe 0.1.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
xscribe-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 edbutlerx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
xscribe-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: xscribe
3
+ Version: 0.1.0
4
+ Summary: Transcribe video and audio to markdown with timestamps. Supports local files and streams.
5
+ Project-URL: Homepage, https://github.com/edbutlerx/xscribe
6
+ Project-URL: Issues, https://github.com/edbutlerx/xscribe/issues
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: audio,markdown,transcription,video,whisper
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: faster-whisper
17
+ Description-Content-Type: text/markdown
18
+
19
+ # xscribe
20
+
21
+ **Download and transcribe any online video in minutes.**
22
+
23
+ Turn any video or audio file into a clean, timestamped markdown transcript. Just point xscribe at a file or stream URL and get a readable transcript — no cloud services, no subscriptions, everything runs locally on your machine.
24
+
25
+ Powered by [faster-whisper](https://github.com/SYSTRAN/faster-whisper).
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install xscribe
31
+ ```
32
+
33
+ You also need **ffmpeg** installed on your system:
34
+
35
+ ```bash
36
+ # macOS
37
+ brew install ffmpeg
38
+
39
+ # Ubuntu/Debian
40
+ sudo apt install ffmpeg
41
+
42
+ # Windows
43
+ winget install ffmpeg
44
+ ```
45
+
46
+ ## Quick start
47
+
48
+ **Transcribe a video file on your computer:**
49
+
50
+ ```bash
51
+ xscribe interview.mp4
52
+ ```
53
+
54
+ This creates `interview.md` in your current folder with the full transcript and timestamps.
55
+
56
+ **Transcribe an online video stream:**
57
+
58
+ ```bash
59
+ xscribe "https://stream.example.com/video/playlist.m3u8"
60
+ ```
61
+
62
+ xscribe will download the video first, then transcribe it. You'll need [yt-dlp](https://github.com/yt-dlp/yt-dlp) installed for this (`brew install yt-dlp` or `pip install yt-dlp`).
63
+
64
+ ## Usage examples
65
+
66
+ ```bash
67
+ # Transcribe a podcast episode you downloaded
68
+ xscribe episode-42.mp3
69
+
70
+ # Transcribe a lecture recording
71
+ xscribe lecture.mov
72
+
73
+ # Transcribe a YouTube stream you grabbed the URL for
74
+ xscribe "https://manifest.googlevideo.com/.../playlist.m3u8"
75
+
76
+ # Use a more accurate model (slower but better for tricky audio)
77
+ xscribe meeting.mp4 -m large-v3
78
+
79
+ # Save the transcript to a specific file
80
+ xscribe keynote.mp4 -o keynote-notes.md
81
+ ```
82
+
83
+ **Supported file types:** mp4, mp3, wav, mov, mkv, webm, m4a, flac, ogg, and anything else ffmpeg can read.
84
+
85
+ ## How to get an .m3u8 URL from any website
86
+
87
+ Most streaming videos use .m3u8 playlist URLs behind the scenes. Here's how to find them:
88
+
89
+ 1. Open the website with the video in Chrome or any browser
90
+ 2. Right-click anywhere on the page and select **Inspect** (or press `F12`)
91
+ 3. Click the **Network** tab in the developer tools panel
92
+ 4. Play the video on the page
93
+ 5. In the Network tab's filter/search bar, type `.m3u8`
94
+ 6. You'll see one or more requests appear — right-click the URL and select **Copy URL**
95
+ 7. Paste it into xscribe: `xscribe "https://...your-copied-url.m3u8"`
96
+
97
+ ## Model sizes
98
+
99
+ xscribe uses OpenAI's Whisper speech recognition. You can choose different model sizes depending on whether you want speed or accuracy:
100
+
101
+ | Model | Flag | Best for |
102
+ |-------|------|----------|
103
+ | Tiny | `-m tiny` | Quick and dirty, when you just need the gist |
104
+ | Base | *(default)* | General use, good balance of speed and quality |
105
+ | Small | `-m small` | Better accuracy, still reasonably fast |
106
+ | Medium | `-m medium` | High accuracy for important transcripts |
107
+ | Large | `-m large-v3` | Best possible accuracy, but slowest |
108
+
109
+ The model downloads automatically the first time you use it and gets cached for future runs.
110
+
111
+ ## Output format
112
+
113
+ xscribe saves transcripts as markdown files with timestamps:
114
+
115
+ ```markdown
116
+ # Transcription
117
+
118
+ **Source:** `interview.mp4`
119
+
120
+ ---
121
+
122
+ **[00:03]** Hello and welcome to the show.
123
+
124
+ **[00:07]** Today we're joined by a special guest...
125
+
126
+ **[01:24]** Let's dive into the first topic.
127
+ ```
128
+
129
+ ## License
130
+
131
+ MIT
@@ -0,0 +1,113 @@
1
+ # xscribe
2
+
3
+ **Download and transcribe any online video in minutes.**
4
+
5
+ Turn any video or audio file into a clean, timestamped markdown transcript. Just point xscribe at a file or stream URL and get a readable transcript — no cloud services, no subscriptions, everything runs locally on your machine.
6
+
7
+ Powered by [faster-whisper](https://github.com/SYSTRAN/faster-whisper).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install xscribe
13
+ ```
14
+
15
+ You also need **ffmpeg** installed on your system:
16
+
17
+ ```bash
18
+ # macOS
19
+ brew install ffmpeg
20
+
21
+ # Ubuntu/Debian
22
+ sudo apt install ffmpeg
23
+
24
+ # Windows
25
+ winget install ffmpeg
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ **Transcribe a video file on your computer:**
31
+
32
+ ```bash
33
+ xscribe interview.mp4
34
+ ```
35
+
36
+ This creates `interview.md` in your current folder with the full transcript and timestamps.
37
+
38
+ **Transcribe an online video stream:**
39
+
40
+ ```bash
41
+ xscribe "https://stream.example.com/video/playlist.m3u8"
42
+ ```
43
+
44
+ xscribe will download the video first, then transcribe it. You'll need [yt-dlp](https://github.com/yt-dlp/yt-dlp) installed for this (`brew install yt-dlp` or `pip install yt-dlp`).
45
+
46
+ ## Usage examples
47
+
48
+ ```bash
49
+ # Transcribe a podcast episode you downloaded
50
+ xscribe episode-42.mp3
51
+
52
+ # Transcribe a lecture recording
53
+ xscribe lecture.mov
54
+
55
+ # Transcribe a YouTube stream you grabbed the URL for
56
+ xscribe "https://manifest.googlevideo.com/.../playlist.m3u8"
57
+
58
+ # Use a more accurate model (slower but better for tricky audio)
59
+ xscribe meeting.mp4 -m large-v3
60
+
61
+ # Save the transcript to a specific file
62
+ xscribe keynote.mp4 -o keynote-notes.md
63
+ ```
64
+
65
+ **Supported file types:** mp4, mp3, wav, mov, mkv, webm, m4a, flac, ogg, and anything else ffmpeg can read.
66
+
67
+ ## How to get an .m3u8 URL from any website
68
+
69
+ Most streaming videos use .m3u8 playlist URLs behind the scenes. Here's how to find them:
70
+
71
+ 1. Open the website with the video in Chrome or any browser
72
+ 2. Right-click anywhere on the page and select **Inspect** (or press `F12`)
73
+ 3. Click the **Network** tab in the developer tools panel
74
+ 4. Play the video on the page
75
+ 5. In the Network tab's filter/search bar, type `.m3u8`
76
+ 6. You'll see one or more requests appear — right-click the URL and select **Copy URL**
77
+ 7. Paste it into xscribe: `xscribe "https://...your-copied-url.m3u8"`
78
+
79
+ ## Model sizes
80
+
81
+ xscribe uses OpenAI's Whisper speech recognition. You can choose different model sizes depending on whether you want speed or accuracy:
82
+
83
+ | Model | Flag | Best for |
84
+ |-------|------|----------|
85
+ | Tiny | `-m tiny` | Quick and dirty, when you just need the gist |
86
+ | Base | *(default)* | General use, good balance of speed and quality |
87
+ | Small | `-m small` | Better accuracy, still reasonably fast |
88
+ | Medium | `-m medium` | High accuracy for important transcripts |
89
+ | Large | `-m large-v3` | Best possible accuracy, but slowest |
90
+
91
+ The model downloads automatically the first time you use it and gets cached for future runs.
92
+
93
+ ## Output format
94
+
95
+ xscribe saves transcripts as markdown files with timestamps:
96
+
97
+ ```markdown
98
+ # Transcription
99
+
100
+ **Source:** `interview.mp4`
101
+
102
+ ---
103
+
104
+ **[00:03]** Hello and welcome to the show.
105
+
106
+ **[00:07]** Today we're joined by a special guest...
107
+
108
+ **[01:24]** Let's dive into the first topic.
109
+ ```
110
+
111
+ ## License
112
+
113
+ MIT
File without changes
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "xscribe"
3
+ version = "0.1.0"
4
+ description = "Transcribe video and audio to markdown with timestamps. Supports local files and streams."
5
+ requires-python = ">=3.10"
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ keywords = ["transcription", "whisper", "video", "audio", "markdown"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
15
+ ]
16
+ dependencies = [
17
+ "faster-whisper",
18
+ ]
19
+
20
+ [project.scripts]
21
+ xscribe = "xscribe:main"
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/edbutlerx/xscribe"
25
+ Issues = "https://github.com/edbutlerx/xscribe/issues"
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["."]
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env python3
2
+ """Video transcription CLI. Transcribes video/audio files to markdown with timestamps."""
3
+
4
+ import argparse
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import threading
10
+ from pathlib import Path
11
+
12
+
13
+ def is_stream_url(path: str) -> bool:
14
+ return path.startswith("http://") or path.startswith("https://") or path.endswith(".m3u8")
15
+
16
+
17
+ def download_stream(url: str, output_dir: str) -> str:
18
+ """Download a stream URL using yt-dlp and return the output file path."""
19
+ output_path = os.path.join(output_dir, "downloaded_video.%(ext)s")
20
+ cmd = [
21
+ "yt-dlp",
22
+ "-o", output_path,
23
+ "--no-playlist",
24
+ url,
25
+ ]
26
+ spinner = ProgressSpinner("Downloading stream...")
27
+ spinner.start()
28
+ result = subprocess.run(cmd, capture_output=True, text=True)
29
+ if result.returncode != 0:
30
+ spinner.stop("✗ Download failed")
31
+ print(f"yt-dlp error: {result.stderr}", file=sys.stderr)
32
+ sys.exit(1)
33
+ spinner.stop("✓ Download complete")
34
+
35
+ # Find the downloaded file
36
+ for f in os.listdir(output_dir):
37
+ if f.startswith("downloaded_video"):
38
+ return os.path.join(output_dir, f)
39
+
40
+ print("Error: could not find downloaded file", file=sys.stderr)
41
+ sys.exit(1)
42
+
43
+
44
+ SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
45
+
46
+
47
+ def get_audio_duration(file_path: str) -> float | None:
48
+ """Get duration of a media file in seconds using ffprobe."""
49
+ try:
50
+ result = subprocess.run(
51
+ ["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
52
+ "-of", "default=noprint_wrappers=1:nokey=1", file_path],
53
+ capture_output=True, text=True,
54
+ )
55
+ return float(result.stdout.strip())
56
+ except (ValueError, FileNotFoundError):
57
+ return None
58
+
59
+
60
+ class ProgressSpinner:
61
+ """Spinner with percentage progress on a single line."""
62
+
63
+ def __init__(self, label: str, total: float | None = None):
64
+ self.label = label
65
+ self.total = total
66
+ self.current = 0.0
67
+ self._stop = threading.Event()
68
+ self._frame = 0
69
+ self._thread = threading.Thread(target=self._spin, daemon=True)
70
+
71
+ def start(self):
72
+ self._thread.start()
73
+
74
+ def update(self, value: float):
75
+ self.current = value
76
+
77
+ def _spin(self):
78
+ while not self._stop.is_set():
79
+ frame = SPINNER_FRAMES[self._frame % len(SPINNER_FRAMES)]
80
+ if self.total and self.total > 0:
81
+ pct = min(self.current / self.total * 100, 100)
82
+ sys.stdout.write(f"\r{frame} {self.label} {pct:.0f}%")
83
+ else:
84
+ sys.stdout.write(f"\r{frame} {self.label}")
85
+ sys.stdout.flush()
86
+ self._frame += 1
87
+ self._stop.wait(0.1)
88
+
89
+ def stop(self, final_message: str = ""):
90
+ self._stop.set()
91
+ self._thread.join()
92
+ sys.stdout.write(f"\r\033[K{final_message}\n")
93
+ sys.stdout.flush()
94
+
95
+
96
+ def format_timestamp(seconds: float) -> str:
97
+ """Format seconds into HH:MM:SS."""
98
+ h = int(seconds // 3600)
99
+ m = int((seconds % 3600) // 60)
100
+ s = int(seconds % 60)
101
+ if h > 0:
102
+ return f"{h:02d}:{m:02d}:{s:02d}"
103
+ return f"{m:02d}:{s:02d}"
104
+
105
+
106
+ def transcribe(file_path: str, model_size: str) -> list[dict]:
107
+ """Transcribe a file using faster-whisper. Returns list of segments."""
108
+ try:
109
+ from faster_whisper import WhisperModel
110
+ except ImportError:
111
+ print("Error: faster-whisper is not installed.", file=sys.stderr)
112
+ print("Install it with: pip install faster-whisper", file=sys.stderr)
113
+ sys.exit(1)
114
+
115
+ duration = get_audio_duration(file_path)
116
+
117
+ spinner = ProgressSpinner("Loading model...", total=None)
118
+ spinner.start()
119
+ model = WhisperModel(model_size, device="auto", compute_type="auto")
120
+ spinner.stop(f"✓ Model loaded: {model_size}")
121
+
122
+ spinner = ProgressSpinner("Transcribing...", total=duration)
123
+ spinner.start()
124
+ segments_gen, info = model.transcribe(file_path, beam_size=5)
125
+
126
+ segments = []
127
+ for segment in segments_gen:
128
+ segments.append({
129
+ "start": segment.start,
130
+ "end": segment.end,
131
+ "text": segment.text.strip(),
132
+ })
133
+ spinner.update(segment.end)
134
+
135
+ spinner.stop(f"✓ Transcription complete ({info.language})")
136
+
137
+ return segments
138
+
139
+
140
+ def write_markdown(segments: list[dict], output_path: str, source: str):
141
+ """Write transcription segments to a markdown file."""
142
+ with open(output_path, "w") as f:
143
+ f.write(f"# Transcription\n\n")
144
+ f.write(f"**Source:** `{source}`\n\n")
145
+ f.write("---\n\n")
146
+
147
+ for seg in segments:
148
+ ts = format_timestamp(seg["start"])
149
+ f.write(f"**[{ts}]** {seg['text']}\n\n")
150
+
151
+ print(f"✓ Saved to: {output_path}")
152
+
153
+
154
+ def main():
155
+ parser = argparse.ArgumentParser(description="Transcribe video/audio to markdown with timestamps.")
156
+ parser.add_argument("input", help="File path or stream URL (m3u8, etc.) to transcribe")
157
+ parser.add_argument("-o", "--output", help="Output markdown file path (default: <input_name>.md)")
158
+ parser.add_argument("-m", "--model", default="base", choices=["tiny", "base", "small", "medium", "large-v3"],
159
+ help="Whisper model size (default: base)")
160
+ args = parser.parse_args()
161
+
162
+ source = args.input
163
+ temp_dir = None
164
+
165
+ try:
166
+ if is_stream_url(source):
167
+ temp_dir = tempfile.mkdtemp(prefix="xscribe_")
168
+ file_path = download_stream(source, temp_dir)
169
+ else:
170
+ file_path = os.path.abspath(source)
171
+ if not os.path.isfile(file_path):
172
+ print(f"Error: file not found: {file_path}", file=sys.stderr)
173
+ sys.exit(1)
174
+
175
+ # Determine output path
176
+ if args.output:
177
+ output_path = os.path.abspath(args.output)
178
+ else:
179
+ base_name = Path(source).stem if not is_stream_url(source) else "transcription"
180
+ output_path = os.path.join(os.getcwd(), f"{base_name}.md")
181
+
182
+ segments = transcribe(file_path, args.model)
183
+
184
+ if not segments:
185
+ print("No speech detected.", file=sys.stderr)
186
+ sys.exit(1)
187
+
188
+ write_markdown(segments, output_path, source)
189
+
190
+ finally:
191
+ # Clean up temp files
192
+ if temp_dir and os.path.exists(temp_dir):
193
+ import shutil
194
+ shutil.rmtree(temp_dir)
195
+
196
+
197
+ if __name__ == "__main__":
198
+ main()