Skip to content

Commit ced6ec2

Browse files
committed
miniflow by smallest.ai
1 parent f9cc5b6 commit ced6ec2

19 files changed

Lines changed: 804 additions & 164 deletions

File tree

.github/workflows/build-quick.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Build MiniFlow DMG (no notarization)
2+
3+
on:
4+
push:
5+
branches: ['**']
6+
workflow_dispatch:
7+
8+
jobs:
9+
build:
10+
runs-on: macos-15
11+
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Get version
17+
id: version
18+
run: |
19+
VERSION=$(grep -m1 'MARKETING_VERSION' MiniflowApp/MiniflowApp.xcodeproj/project.pbxproj \
20+
| sed 's/.*= *//;s/;//;s/ *//')
21+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.11"
27+
28+
- name: Create venv & install dependencies
29+
run: |
30+
cd miniflow-engine
31+
python3 -m venv venv
32+
source venv/bin/activate
33+
pip install --upgrade pip
34+
pip install -r requirements.txt
35+
36+
- name: Build (backend + Swift app + DMG)
37+
run: |
38+
chmod +x build_all.sh build_backend.sh build_dmg.sh
39+
./build_all.sh
40+
env:
41+
CONFIG: Release
42+
43+
- name: Upload DMG
44+
uses: actions/upload-artifact@v4
45+
with:
46+
name: MiniFlow-${{ steps.version.outputs.version }}-unsigned
47+
path: build/MiniFlow-${{ steps.version.outputs.version }}.dmg
48+
retention-days: 7

.github/workflows/build.yml

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Build MiniFlow DMG
22

33
on:
44
push:
5-
branches: [main, ui, development, ai]
5+
branches: [main, ui, development, ai, ai-websocket, groq-test]
66
workflow_dispatch:
77

88
jobs:
@@ -13,6 +13,41 @@ jobs:
1313
- name: Checkout
1414
uses: actions/checkout@v4
1515

16+
- name: Validate signing and notarization secrets
17+
env:
18+
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
19+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
20+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
21+
APPLE_ID: ${{ secrets.APPLE_ID }}
22+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
23+
run: |
24+
set -euo pipefail
25+
missing=0
26+
for name in \
27+
APPLE_CERTIFICATE_BASE64 \
28+
APPLE_CERTIFICATE_PASSWORD \
29+
APPLE_TEAM_ID \
30+
APPLE_ID \
31+
APPLE_APP_SPECIFIC_PASSWORD; do
32+
if [ -z "${!name:-}" ]; then
33+
echo "✗ Missing required secret: $name"
34+
missing=1
35+
else
36+
echo "✓ Found secret: $name"
37+
fi
38+
done
39+
if [ "$missing" -ne 0 ]; then
40+
exit 1
41+
fi
42+
if ! printf '%s' "$APPLE_CERTIFICATE_BASE64" | base64 --decode >/dev/null 2>&1; then
43+
echo "✗ APPLE_CERTIFICATE_BASE64 is not valid base64"
44+
exit 1
45+
fi
46+
if ! printf '%s' "$APPLE_ID" | grep -Eq '^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$'; then
47+
echo "✗ APPLE_ID does not look like an email address"
48+
exit 1
49+
fi
50+
1651
- name: Get version
1752
id: version
1853
run: |
@@ -33,12 +68,44 @@ jobs:
3368
pip install --upgrade pip
3469
pip install -r requirements.txt
3570
71+
- name: Import signing certificate
72+
env:
73+
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
74+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
75+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
76+
run: |
77+
set -euo pipefail
78+
KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
79+
KEYCHAIN_PASSWORD=$(openssl rand -hex 16)
80+
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
81+
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"
82+
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode -o "$RUNNER_TEMP/certificate.p12"
83+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
84+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
85+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
86+
security import "$RUNNER_TEMP/certificate.p12" \
87+
-k "$KEYCHAIN_PATH" \
88+
-P "$APPLE_CERTIFICATE_PASSWORD" \
89+
-T /usr/bin/codesign
90+
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
91+
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
92+
echo "→ Available signing identities:"
93+
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
94+
if ! security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep -q "Developer ID Application"; then
95+
echo "✗ Developer ID Application identity not found after import"
96+
exit 1
97+
fi
98+
echo "✓ Signing certificate imported"
99+
36100
- name: Build (backend + Swift app + DMG)
37101
run: |
38102
chmod +x build_all.sh build_backend.sh build_dmg.sh
39103
./build_all.sh
40104
env:
41105
CONFIG: Release
106+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
107+
APPLE_ID: ${{ secrets.APPLE_ID }}
108+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
42109

43110
- name: Upload DMG
44111
uses: actions/upload-artifact@v4

.github/workflows/release.yml

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,47 @@ on:
1414

1515
jobs:
1616
build:
17-
runs-on: macos-14
17+
runs-on: macos-15
1818

1919
steps:
2020
- name: Checkout
2121
uses: actions/checkout@v4
2222

23+
- name: Validate signing and notarization secrets
24+
env:
25+
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
26+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
27+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
28+
APPLE_ID: ${{ secrets.APPLE_ID }}
29+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
30+
run: |
31+
set -euo pipefail
32+
missing=0
33+
for name in \
34+
APPLE_CERTIFICATE_BASE64 \
35+
APPLE_CERTIFICATE_PASSWORD \
36+
APPLE_TEAM_ID \
37+
APPLE_ID \
38+
APPLE_APP_SPECIFIC_PASSWORD; do
39+
if [ -z "${!name:-}" ]; then
40+
echo "✗ Missing required secret: $name"
41+
missing=1
42+
else
43+
echo "✓ Found secret: $name"
44+
fi
45+
done
46+
if [ "$missing" -ne 0 ]; then
47+
exit 1
48+
fi
49+
if ! printf '%s' "$APPLE_CERTIFICATE_BASE64" | base64 --decode >/dev/null 2>&1; then
50+
echo "✗ APPLE_CERTIFICATE_BASE64 is not valid base64"
51+
exit 1
52+
fi
53+
if ! printf '%s' "$APPLE_ID" | grep -Eq '^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$'; then
54+
echo "✗ APPLE_ID does not look like an email address"
55+
exit 1
56+
fi
57+
2358
- name: Get version
2459
id: version
2560
run: |
@@ -42,8 +77,42 @@ jobs:
4277
miniflow-engine/venv/bin/pip install --upgrade pip
4378
miniflow-engine/venv/bin/pip install -r miniflow-engine/requirements.txt
4479
80+
- name: Import signing certificate
81+
env:
82+
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
83+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
84+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
85+
run: |
86+
set -euo pipefail
87+
KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
88+
KEYCHAIN_PASSWORD=$(openssl rand -hex 16)
89+
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
90+
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"
91+
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode -o "$RUNNER_TEMP/certificate.p12"
92+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
93+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
94+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
95+
security import "$RUNNER_TEMP/certificate.p12" \
96+
-k "$KEYCHAIN_PATH" \
97+
-P "$APPLE_CERTIFICATE_PASSWORD" \
98+
-T /usr/bin/codesign
99+
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
100+
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
101+
echo "→ Available signing identities:"
102+
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
103+
if ! security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep -q "Developer ID Application"; then
104+
echo "✗ Developer ID Application identity not found after import"
105+
exit 1
106+
fi
107+
echo "✓ Signing certificate imported"
108+
45109
- name: Build (backend + Swift app + DMG)
46110
run: VERSION=${{ steps.version.outputs.version }} bash build_all.sh
111+
env:
112+
CONFIG: Release
113+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
114+
APPLE_ID: ${{ secrets.APPLE_ID }}
115+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
47116

48117
- name: Upload DMG artifact
49118
uses: actions/upload-artifact@v4

MiniflowApp/MiniflowApp.xcodeproj/project.pbxproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@
404404
CURRENT_PROJECT_VERSION = 1;
405405
DEAD_CODE_STRIPPING = YES;
406406
ENABLE_APP_SANDBOX = NO;
407-
ENABLE_HARDENED_RUNTIME = NO;
407+
ENABLE_HARDENED_RUNTIME = YES;
408408
GENERATE_INFOPLIST_FILE = NO;
409409
INFOPLIST_FILE = MiniflowApp/Info.plist;
410410
LD_RUNPATH_SEARCH_PATHS = (
@@ -424,13 +424,14 @@
424424
buildSettings = {
425425
CODE_SIGN_ENTITLEMENTS = MiniflowApp/MiniflowApp.entitlements;
426426
CODE_SIGN_IDENTITY = "-";
427+
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
427428
CODE_SIGN_STYLE = Manual;
428429
DEVELOPMENT_TEAM = "";
429430
COMBINE_HIDPI_IMAGES = YES;
430431
CURRENT_PROJECT_VERSION = 1;
431432
DEAD_CODE_STRIPPING = YES;
432433
ENABLE_APP_SANDBOX = NO;
433-
ENABLE_HARDENED_RUNTIME = NO;
434+
ENABLE_HARDENED_RUNTIME = YES;
434435
GENERATE_INFOPLIST_FILE = NO;
435436
INFOPLIST_FILE = MiniflowApp/Info.plist;
436437
LD_RUNPATH_SEARCH_PATHS = (

MiniflowApp/MiniflowApp/AudioCaptureService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ final class AudioCaptureService {
99
private var isRunning = false
1010
private var accumulatedPCM = Data()
1111

12+
/// Called with each ~100ms PCM chunk as it's captured (for streaming)
13+
var onChunk: ((Data) -> Void)?
14+
1215
private init() {
1316
// When the user changes audio device (plug/unplug headphones, switch input),
1417
// AVAudioEngine stops automatically. Restart capture so audio keeps flowing.
@@ -75,6 +78,7 @@ final class AudioCaptureService {
7578
guard let converted = convert(buffer, from: nativeFormat, to: format) else { return }
7679
guard let data = pcmData(from: converted) else { return }
7780
accumulatedPCM.append(data)
81+
onChunk?(data)
7882
}
7983

8084
// MARK: - WAV encoding

MiniflowApp/MiniflowApp/Bridge/EventStream.swift

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ final class EventStream: ObservableObject {
1919
private var task: URLSessionWebSocketTask?
2020
private var reconnectWorkItem: DispatchWorkItem?
2121

22+
// Pending transcription continuations keyed by request ID
23+
private var transcriptContinuations: [String: CheckedContinuation<String, Error>] = [:]
24+
2225
private init() {}
2326

2427
// MARK: - Connection
@@ -62,16 +65,78 @@ final class EventStream: ObservableObject {
6265
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: item)
6366
}
6467

68+
// MARK: - Streaming transcription
69+
70+
@Published var partialTranscript: String = ""
71+
private var activeSessionID: String?
72+
73+
func startTranscription(bundleID: String?) {
74+
let sessionID = UUID().uuidString
75+
activeSessionID = sessionID
76+
partialTranscript = ""
77+
sendMessage([
78+
"type": "start_transcription",
79+
"id": sessionID,
80+
"bundleID": bundleID as Any,
81+
])
82+
}
83+
84+
func sendAudioChunk(_ pcm: Data) {
85+
guard let sessionID = activeSessionID else { return }
86+
sendMessage([
87+
"type": "audio_chunk",
88+
"id": sessionID,
89+
"data": pcm.base64EncodedString(),
90+
])
91+
}
92+
93+
func stopTranscription() async throws -> String {
94+
guard let sessionID = activeSessionID else {
95+
throw URLError(.unknown)
96+
}
97+
activeSessionID = nil
98+
sendMessage(["type": "stop_transcription", "id": sessionID])
99+
return try await withCheckedThrowingContinuation { continuation in
100+
transcriptContinuations[sessionID] = continuation
101+
}
102+
}
103+
104+
private func sendMessage(_ payload: [String: Any]) {
105+
guard let data = try? JSONSerialization.data(withJSONObject: payload),
106+
let text = String(data: data, encoding: .utf8) else { return }
107+
task?.send(.string(text)) { _ in }
108+
}
109+
65110
// MARK: - Event dispatch
66111

67112
private func dispatch(_ text: String) {
68113
guard
69114
let data = text.data(using: .utf8),
70115
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
71-
let event = json["event"] as? String,
72-
let payload = json["payload"]
116+
let event = json["event"] as? String
73117
else { return }
74118

119+
// Partial transcript — update live display
120+
if event == "partial_transcript",
121+
let payload = json["payload"] as? [String: Any],
122+
let text = payload["text"] as? String {
123+
DispatchQueue.main.async { self.partialTranscript = text }
124+
return
125+
}
126+
127+
// Final transcript — resume the waiting continuation
128+
if event == "transcript",
129+
let reqID = json["id"] as? String,
130+
let payload = json["payload"] as? [String: Any],
131+
let transcript = payload["transcript"] as? String {
132+
DispatchQueue.main.async { self.partialTranscript = "" }
133+
let continuation = transcriptContinuations.removeValue(forKey: reqID)
134+
continuation?.resume(returning: transcript)
135+
return
136+
}
137+
138+
guard let payload = json["payload"] else { return }
139+
75140
DispatchQueue.main.async {
76141
switch event {
77142
case "agent-status":

0 commit comments

Comments
 (0)