Skip to content
This repository was archived by the owner on Jun 17, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,6 @@ jobs:
- name: Install dependencies
run: npm ci

# Cache the downloaded caption model so we don't re-fetch from HuggingFace every run
# (and to avoid 429s when the platform matrix builds hit it at once).
- name: Cache caption assets
uses: actions/cache@v4
with:
path: caption-assets
key: caption-assets-${{ hashFiles('scripts/fetch-caption-model.mjs') }}

- name: Build Windows app
run: npm run build:win
env:
Expand Down Expand Up @@ -86,14 +78,6 @@ jobs:
env:
npm_config_build_from_source: "false"

# ─── Cache caption assets ─────────────────────────────────
# Avoid re-fetching the Whisper model from HuggingFace every run (and 429s under the matrix).
- name: Cache caption assets
uses: actions/cache@v4
with:
path: caption-assets
key: caption-assets-${{ hashFiles('scripts/fetch-caption-model.mjs') }}

# ─── Import Code Signing Certificate ──────────────────────
# This is the KEY step that makes CI signing work.
# We create a temporary keychain, import the .p12 cert into it,
Expand Down Expand Up @@ -272,14 +256,6 @@ jobs:
- name: Install pacman build dependencies
run: sudo apt-get update && sudo apt-get install -y libarchive-tools

# Cache the downloaded caption model so we don't re-fetch from HuggingFace every run
# (and to avoid 429s when the platform matrix builds hit it at once).
- name: Cache caption assets
uses: actions/cache@v4
with:
path: caption-assets
key: caption-assets-${{ hashFiles('scripts/fetch-caption-model.mjs') }}

- name: Build Linux app
run: npm run build:linux
env:
Expand Down
8 changes: 5 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ pnpm-debug.log*
lerna-debug.log*

node_modules
.worktrees/
dist
dist-electron
dist-ssr
/main.js
/preload.mjs
/index-*.js
*.local
.env

Expand All @@ -20,6 +24,7 @@ dist-ssr
/electron/native/screencapturekit/.build/
/electron/native/screencapturekit/.swiftpm/
/electron/native/bin/
/electron/native/captions/*/

# Native macOS generated files
DerivedData/
Expand Down Expand Up @@ -63,6 +68,3 @@ result-*
#others

**/*.import

# Auto-caption model + ORT wasm — regenerated at build by scripts/fetch-caption-model.mjs
/caption-assets/
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ If you don't want to pay $29/month for Screen Studio but want a version that doe

Screen Studio is an awesome product and this is definitely not a 1:1 clone. If you just want something fully free and open source, this project should cover most of your needs.

**100% free** for both **personal** and **commercial** use. Use it, modify it, distribute it. Please respect the License.
**100% free** for both **personal** and **commercial** use. Use it, modify it, distribute it. Please respect the License.

> [!NOTE]
>Software should be accessible. OpenScreen has no paid tiers, premium features, upsells, or functionality locked behind a paywall.
Expand All @@ -40,7 +40,11 @@ Screen Studio is an awesome product and this is definitely not a 1:1 clone. If y
- Wallpapers, solid colors, gradients, or your own background image.
- Motion blur.
- Crop, trim, and per-segment speed control on the timeline.
- Blur effects to hide sensitive parts of the screen.
- Cursor and click highlighting.
- Text, arrow, and image annotations, with text animation presets.
- Offline auto-captions on macOS release builds, generated locally with bundled whisper.cpp helpers and the default `ggml-small` model.
- Save and reopen projects without re-recording.
- Timeline snapping guides and an audio waveform to make trimming easier.
- Customizable keyboard shortcuts.
- Export to MP4 or GIF in multiple aspect ratios and resolutions.
Expand Down Expand Up @@ -161,6 +165,8 @@ Everything in the editor and export is the same on macOS, Windows, and Linux: zo
- **Windows**: works out of the box.
- **Linux**: needs PipeWire (default on Ubuntu 22.04+, Fedora 34+). Older PulseAudio-only setups may not capture system audio (mic should still work).

Offline auto-captions currently depend on packaged caption runtime assets. macOS release builds include the local whisper.cpp CLI, ffmpeg, and `ggml-small` model; other platforms report captions as unavailable until their helper assets are packaged.

---

## License
Expand Down
71 changes: 35 additions & 36 deletions electron-builder.json5
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@
// .node binaries can't be dlopen'd from inside an asar — must live unpacked.
"asarUnpack": [
"**/*.node"
],
"productName": "Openscreen",
// Fetch the auto-caption model + ORT wasm into caption-assets/ before packaging (idempotent).
"beforePack": "scripts/before-pack.cjs",
"npmRebuild": true,
// sharp ships ABI-stable (napi) prebuilt binaries with bundled libvips. Building it from source
// needs a system libvips we don't provide and breaks on CI/local ("vips-cpp.42 not found"), so we
// let electron-builder use the prebuilt instead of recompiling.
"buildDependenciesFromSource": false,
],
"productName": "Openscreen",
"npmRebuild": true,
// sharp ships ABI-stable (napi) prebuilt binaries with bundled libvips. Building it from source
// needs a system libvips we don't provide and breaks on CI/local ("vips-cpp.42 not found"), so we
// let electron-builder use the prebuilt instead of recompiling.
"buildDependenciesFromSource": false,
"compression": "normal",
"directories": {
"output": "release/${version}"
Expand All @@ -29,43 +27,44 @@
"!CONTRIBUTING.md",
"!LICENSE"
],
// Asset layout contract: "wallpapers/" and "cursors/" under resourcesPath must
// align with assetBaseDir in electron/preload.ts (packaged branch).
// Asset layout contract: "wallpapers/" and "cursors/" under resourcesPath must
// align with assetBaseDir in electron/preload.ts (packaged branch).
"extraResources": [
{
"from": "public/wallpapers",
"to": "wallpapers"
},
{
"from": "public/cursors",
"to": "cursors"
},
{
"from": "caption-assets",
"to": "caption-assets"
}
],
},
{
"from": "public/cursors",
"to": "cursors"
}
],

"mac": {
"notarize": false,
"hardenedRuntime": true,
"entitlements": "macos.entitlements",
"entitlementsInherit": "macos.entitlements",
"target": [
{
"target": "dmg",
"arch": ["x64", "arm64"]
}
],
"icon": "icons/icons/mac/icon.icns",
"artifactName": "${productName}-Mac-${arch}-${version}-Installer.${ext}",
"extraResources": [
{
"from": "electron/native/bin",
"to": "electron/native/bin",
"filter": ["darwin-*/*"]
}
],
"target": [
{
"target": "dmg",
"arch": ["x64", "arm64"]
}
],
"icon": "icons/icons/mac/icon.icns",
"artifactName": "${productName}-Mac-${arch}-${version}-Installer.${ext}",
"extraResources": [
{
"from": "electron/native/bin",
"to": "electron/native/bin",
"filter": ["darwin-*/*"]
},
{
"from": "electron/native/captions",
"to": "electron/native/captions",
"filter": ["darwin-*/*"]
}
],
Comment on lines +48 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check mac build arch defaults/flags =="
rg -n -C3 --no-heading 'ARCHS|--x64|--arm64|electron-builder --mac' scripts/build_macos.sh

echo
echo "== Check packaging config arch/resource filters =="
rg -n -C2 --no-heading '"target"|"arch"|darwin-arm64|darwin-x64|electron/native/captions|electron/native/bin' electron-builder.json5

echo
echo "== Check docs/automation mentions of mac x64 support =="
rg -n -C2 --no-heading 'mac.*x64|darwin-x64|--x64|universal|arm64-only' README.md .github 2>/dev/null || true

Repository: siddharthvaddem/openscreen

Length of output: 2367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect scripts/build_macos.sh around arch build + artifact handling =="
rg -n "ARCHS=|\-\-mac|\-\-dir|DMG_NAME|DMG_PATH|APP_BUNDLE|release/" scripts/build_macos.sh
echo
# show the relevant region (best-effort)
sed -n '1,140p' scripts/build_macos.sh
echo
sed -n '140,260p' scripts/build_macos.sh

echo
echo "== Check electron-builder.json5 for any darwin-x64/universal resource handling =="
rg -n --no-heading "darwin-x64|x64|universal|darwin-arm64|extraResources|macos" electron-builder.json5

echo
echo "== Show the full mac section of electron-builder.json5 (roughly) =="
# Print from mac: to next top-level key (best effort)
python3 - <<'PY'
import re, pathlib
p=pathlib.Path("electron-builder.json5")
s=p.read_text(encoding="utf-8", errors="ignore").splitlines()
start=None
for i,line in enumerate(s):
    if re.match(r'^\s*"mac"\s*:', line):
        start=i
        break
if start is None:
    raise SystemExit("mac section not found")
# find next top-level key after start
end=len(s)
for i in range(start+1,len(s)):
    if re.match(r'^\s*"[a-zA-Z]+"\s*:\s*{?', s[i]):
        end=i
        break
for line in s[start:end]:
    print(line)
PY

Repository: siddharthvaddem/openscreen

Length of output: 9489


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect electron/native assets layout =="
# list only a few levels to keep output small
ls -la electron/native 2>/dev/null || true
find electron/native/bin -maxdepth 3 -type d 2>/dev/null | head -n 200 || true
find electron/native/captions -maxdepth 3 -type d 2>/dev/null | head -n 200 || true

echo
echo "== Look for darwin-x64 assets presence =="
rg -n --hidden --no-messages "darwin-x64" . || true
rg -n --hidden --no-messages "darwin-arm64" . || true

echo
echo "== Search for code that loads electron/native/bin/captions =="
rg -n --hidden --no-messages "native/(bin|captions)|electron/native/(bin|captions)|captions|darwin-(arm64|x64)" . || true

Repository: siddharthvaddem/openscreen

Length of output: 12329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== electron-builder.json5 length + mac block with context =="
wc -l electron-builder.json5
rg -n --no-heading '^\s*"mac"\s*:' electron-builder.json5
rg -n --no-heading '"extraResources"|^\s*"target"|darwin-arm64|darwin-x64' electron-builder.json5
echo
python3 - <<'PY'
import pathlib,re
p=pathlib.Path("electron-builder.json5")
lines=p.read_text(encoding="utf-8",errors="ignore").splitlines()
# find mac block start
start=None
for i,l in enumerate(lines):
    if re.match(r'^\s*"mac"\s*:\s*{', l):
        start=i; break
if start is None: 
    print("mac block not found"); raise SystemExit(1)
# print until next top-level key at same indent (starts with "  "??) approximate by regex for ^\s*"[a-zA-Z]+"\s*:
end=len(lines)
for j in range(start+1,len(lines)):
    if re.match(r'^\s*"[a-zA-Z]+"\s*:\s*{?', lines[j]) and j>start:
        end=j
        break
for k in range(start, min(end, start+120)):
    print(f"{k+1:4d}:{lines[k]}")
PY

echo
echo "== Inspect electron/captions/whisper.ts asset path + platformArch logic =="
rg -n --no-heading "resourcesPath|platformArch|darwin-arm64|darwin-x64|native/captions" electron/captions/whisper.ts
python3 - <<'PY'
import pathlib
p=pathlib.Path("electron/captions/whisper.ts")
s=p.read_text(encoding="utf-8",errors="ignore").splitlines()
# print around lines referenced earlier (rough)
for idx in range(60,120):
    if idx < len(s):
        print(f"{idx+1:4d}:{s[idx]}")
PY

echo
echo "== Inspect cursor helper resolution fallback =="
rg -n --no-heading "OPENSCREEN_MAC_CURSOR_HELPER_EXE|openscreen-macos-cursor-helper|archTag|darwin-arm64|darwin-x64" electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts
python3 - <<'PY'
import pathlib
p=pathlib.Path("electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts")
s=p.read_text(encoding="utf-8",errors="ignore").splitlines()
for idx in range(1,120):
    if idx < len(s):
        print(f"{idx+1:4d}:{s[idx]}")
PY

Repository: siddharthvaddem/openscreen

Length of output: 7171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect whisper.ts where whisperPath/modelPath are used =="
rg -n --no-heading "whisperPath|modelPath|resolveCaptionTools|OPENSCREEN_WHISPER" electron/captions/whisper.ts
echo
python3 - <<'PY'
import pathlib
p=pathlib.Path("electron/captions/whisper.ts")
s=p.read_text(encoding="utf-8",errors="ignore").splitlines()
# print ranges likely around resolveCaptionTools usage
for start in [100,140,180,220,260,300]:
    end=min(len(s), start+80)
    print("\n--- lines",start+1,"-",end,"---")
    for i in range(start,end):
        print(f"{i+1:4d}:{s[i]}")
PY

Repository: siddharthvaddem/openscreen

Length of output: 10725


Flag arm64-only native/captions filters as lowkey risky for x64 runtime

electron-builder.json5 hard-pins mac dmg to arm64 and only includes extraResources with filter: ["darwin-arm64/**/*"] for both electron/native/bin and electron/native/captions, even though scripts/build_macos.sh builds an x64 .app. so the x64 bundle won’t contain the darwin-x64 caption/native assets, and electron/captions/whisper.ts will look for ${request.platform}-${request.arch} (darwin-x64 for x64) and return Local caption tools are not available when the tools/models aren’t found (no cross-arch fallback). kinda cursed—this shows up as “captions unavailable” only on x64.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron-builder.json5` around lines 41 - 60, The build config currently
ships only darwin-arm64 caption/native assets and targets arm64, causing
electron/captions/whisper.ts to fail for darwin-x64 runtime; update
electron-builder.json5 so extraResources filters include the x64 variants (e.g.
add "darwin-x64/**/*" or a wildcard like "darwin-*/**/*" for both
electron/native/bin and electron/native/captions) and/or add x64 to the mac
target arches so the bundle contains darwin-x64 assets; confirm
scripts/build_macos.sh outputs match these arch names and that
electron/captions/whisper.ts will find `${request.platform}-${request.arch}`
(e.g. darwin-x64) at runtime.

"extendInfo": {
"NSAudioCaptureUsageDescription": "OpenScreen needs audio capture permission to record system audio.",
"NSMicrophoneUsageDescription": "OpenScreen needs microphone access to record voice audio.",
Expand Down
55 changes: 55 additions & 0 deletions electron/captions/jobs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it, vi } from "vitest";
import { CaptionJobRegistry } from "./jobs";

describe("CaptionJobRegistry", () => {
it("cleans up jobs after completion", async () => {
const registry = new CaptionJobRegistry({
generate: vi.fn(async (request) => ({
jobId: request.jobId,
status: "success",
segments: [],
})),
});

await expect(registry.start({ jobId: "job-1", videoPath: "/tmp/video.webm" })).resolves.toEqual(
{
jobId: "job-1",
status: "success",
segments: [],
},
);
expect(registry.has("job-1")).toBe(false);
});

it("aborts an active job", async () => {
let signal: AbortSignal | undefined;
const registry = new CaptionJobRegistry({
generate: vi.fn(async (request) => {
signal = request.signal;
return { jobId: request.jobId, status: "cancelled", segments: [] };
}),
});

const promise = registry.start({
jobId: "job-2",
videoPath: "/tmp/video.webm",
});
const cancelResult = registry.cancel("job-2");
const result = await promise;

expect(cancelResult).toEqual({ success: true, cancelled: true });
expect(signal?.aborted).toBe(true);
expect(result.status).toBe("cancelled");
});

it("reports missing jobs as not cancelled", () => {
const registry = new CaptionJobRegistry({
generate: vi.fn(),
});

expect(registry.cancel("missing")).toEqual({
success: true,
cancelled: false,
});
});
});
53 changes: 53 additions & 0 deletions electron/captions/jobs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { CaptionGenerationResult } from "../../src/lib/captions";

type CaptionJobStartRequest = {
jobId: string;
videoPath: string;
language?: string;
};

type CaptionJobGenerateRequest = CaptionJobStartRequest & {
signal: AbortSignal;
};

type CaptionJobRegistryDeps = {
generate: (request: CaptionJobGenerateRequest) => Promise<CaptionGenerationResult>;
};

export class CaptionJobRegistry {
private jobs = new Map<string, AbortController>();

constructor(private deps: CaptionJobRegistryDeps) {}

has(jobId: string) {
return this.jobs.has(jobId);
}

async start(request: CaptionJobStartRequest): Promise<CaptionGenerationResult> {
this.cancel(request.jobId);
const controller = new AbortController();
this.jobs.set(request.jobId, controller);

try {
return await this.deps.generate({
...request,
signal: controller.signal,
});
} finally {
if (this.jobs.get(request.jobId) === controller) {
this.jobs.delete(request.jobId);
}
}
}

cancel(jobId: string) {
const controller = this.jobs.get(jobId);
if (!controller) {
return { success: true, cancelled: false };
}

controller.abort();
this.jobs.delete(jobId);
return { success: true, cancelled: true };
}
}
Loading