Skip to content

Commit 832e57c

Browse files
authored
Merge branch 'main' into renovate/bun-1.x
2 parents 8ad042c + b545f82 commit 832e57c

13 files changed

Lines changed: 1065 additions & 121 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name: CI
33
on:
44
push:
55

6+
env:
7+
MISE_DISABLE_TOOLS: ffmpeg
8+
69
jobs:
710
build-and-test:
811
runs-on: ubuntu-latest

mise.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
[tools]
22
bun = "1.3.5"
33
lefthook = "2.0.13"
4+
python = "3.14.2"
5+
ffmpeg = "latest"
6+
uv = "latest"
47

58
[settings]
69
experimental = true
@@ -43,3 +46,7 @@ description = "Upload assets (images, audio) to R2 and update manifest"
4346
[tasks.deploy]
4447
run = "wrangler deploy"
4548
depends = ["build"]
49+
50+
[tasks.transcribe]
51+
run = "uv run --with openai-whisper python scripts/transcribe.py"
52+
description = "Generate word-level transcript from audio/video"

scripts/transcribe.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate word-level transcript from audio/video using OpenAI Whisper.
4+
5+
Usage:
6+
python scripts/transcribe.py <input-file> <output-json> [model-size]
7+
8+
Example:
9+
python scripts/transcribe.py \
10+
public/assets/talks/codegen-in-rust/audio.m4a \
11+
public/transcripts/t2468.json \
12+
medium
13+
"""
14+
15+
import sys
16+
import json
17+
from pathlib import Path
18+
19+
def transcribe_file(input_path: str, output_path: str, model_size: str = "base"):
20+
"""
21+
Transcribe audio/video file with word-level timestamps.
22+
23+
Args:
24+
input_path: Path to audio/video file
25+
output_path: Path to output JSON file
26+
model_size: Whisper model size (tiny, base, small, medium, large)
27+
"""
28+
try:
29+
import whisper
30+
except ImportError:
31+
print("Error: openai-whisper is not installed.")
32+
print("Install it with: pip install openai-whisper")
33+
sys.exit(1)
34+
35+
print(f"Loading Whisper model: {model_size}")
36+
model = whisper.load_model(model_size)
37+
38+
print(f"Transcribing: {input_path}")
39+
print("This may take a while depending on file length and model size...")
40+
41+
result = model.transcribe(
42+
input_path,
43+
word_timestamps=True, # Critical for word-level highlighting
44+
language="en" # Can be removed for auto-detection
45+
)
46+
47+
# Build transcript structure
48+
transcript = {
49+
"language": result.get("language", "en"),
50+
"duration": 0,
51+
"segments": []
52+
}
53+
54+
total_words = 0
55+
56+
for segment in result.get("segments", []):
57+
seg_data = {
58+
"start": round(segment["start"], 3),
59+
"end": round(segment["end"], 3),
60+
"text": segment["text"].strip(),
61+
"words": []
62+
}
63+
64+
# Extract word-level timestamps if available
65+
if "words" in segment:
66+
for word in segment["words"]:
67+
seg_data["words"].append({
68+
"word": word["word"].strip(),
69+
"start": round(word["start"], 3),
70+
"end": round(word["end"], 3)
71+
})
72+
total_words += 1
73+
74+
transcript["segments"].append(seg_data)
75+
transcript["duration"] = max(transcript["duration"], seg_data["end"])
76+
77+
# Ensure output directory exists
78+
output_file = Path(output_path)
79+
output_file.parent.mkdir(parents=True, exist_ok=True)
80+
81+
# Write JSON
82+
with open(output_file, 'w', encoding='utf-8') as f:
83+
json.dump(transcript, f, indent=2, ensure_ascii=False)
84+
85+
print(f"\n✓ Transcript saved to: {output_path}")
86+
print(f" Language: {transcript['language']}")
87+
print(f" Duration: {transcript['duration']:.1f}s ({transcript['duration'] / 60:.1f}min)")
88+
print(f" Segments: {len(transcript['segments'])}")
89+
print(f" Words: {total_words}")
90+
91+
if __name__ == "__main__":
92+
if len(sys.argv) < 3:
93+
print(__doc__)
94+
sys.exit(1)
95+
96+
input_file = sys.argv[1]
97+
output_file = sys.argv[2]
98+
model = sys.argv[3] if len(sys.argv) > 3 else "base"
99+
100+
if not Path(input_file).exists():
101+
print(f"Error: Input file not found: {input_file}")
102+
sys.exit(1)
103+
104+
transcribe_file(input_file, output_file, model)

scripts/upload-assets.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/usr/bin/env bun
22

33
/**
4-
* Upload assets (images, audio) from public/assets to Cloudflare R2
4+
* Upload assets (images, audio, transcripts) from public/assets to Cloudflare R2
55
*
66
* This script:
7-
* - Scans public/assets for media files (images: png, jpg, jpeg, webp, gif, svg; audio: m4a, mp3, wav, ogg)
7+
* - Scans public/assets for media files (images: png, jpg, jpeg, webp, gif, svg; audio: m4a, mp3, wav, ogg; transcripts: json)
88
* - Generates SHA-256 hash for each file
99
* - Uploads to R2 with content-addressable key (hash.ext)
1010
* - Creates manifest mapping original paths to R2 URLs
@@ -30,11 +30,11 @@ import { join, relative } from "path";
3030
const BUCKET_NAME = "just-be-dev-assets";
3131
const CUSTOM_DOMAIN = "https://assets.just-be.dev";
3232
const ASSETS_DIR = join(import.meta.dir, "../public/assets");
33-
const MANIFEST_PATH = join(import.meta.dir, "../src/content/image-manifest.json");
33+
const MANIFEST_PATH = join(import.meta.dir, "../src/content/manifest.json");
3434

3535
interface AssetManifest {
3636
version: string;
37-
images: Record<string, {
37+
assets: Record<string, {
3838
hash: string;
3939
size: number;
4040
ext: string;
@@ -51,7 +51,9 @@ async function findAssets(dir: string): Promise<string[]> {
5151
// Images
5252
'.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg',
5353
// Audio
54-
'.m4a', '.mp3', '.wav', '.ogg', '.aac', '.flac'
54+
'.m4a', '.mp3', '.wav', '.ogg', '.aac', '.flac',
55+
// Transcripts
56+
'.json'
5557
];
5658
const results: string[] = [];
5759

@@ -96,7 +98,7 @@ async function uploadAssets() {
9698

9799
const manifest: AssetManifest = {
98100
version: "1.0",
99-
images: {},
101+
assets: {},
100102
};
101103

102104
let uploadCount = 0;
@@ -122,7 +124,7 @@ async function uploadAssets() {
122124
}
123125

124126
const stats = await Bun.file(assetPath).stat();
125-
manifest.images[originalPath] = {
127+
manifest.assets[originalPath] = {
126128
hash,
127129
size: stats.size,
128130
ext,

0 commit comments

Comments
 (0)