Compare commits
21 Commits
sidecar-v1
...
v2.0.15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d220158dd7 | ||
|
|
8670e19acc | ||
|
|
812cc4ac5e | ||
|
|
4aa19eee86 | ||
|
|
b8dfe0f1ba | ||
|
|
5837b97a20 | ||
|
|
ab09a3e9da | ||
|
|
5343a28a08 | ||
|
|
f0bf026133 | ||
|
|
37a029d1c6 | ||
|
|
5ec030387f | ||
|
|
4d9bdba903 | ||
|
|
a7a3bcd102 | ||
|
|
115d93482a | ||
|
|
fb672cbaef | ||
|
|
d8c79be094 | ||
|
|
2811f5bb9c | ||
|
|
30127d68e7 | ||
|
|
ae61c8c75a | ||
|
|
2654200fe9 | ||
|
|
cae0c0b265 |
@@ -36,9 +36,6 @@ jobs:
|
|||||||
name: Bump version and tag
|
name: Bump version and tag
|
||||||
needs: test
|
needs: test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
new_version: ${{ steps.bump.outputs.new_version }}
|
|
||||||
tag: ${{ steps.bump.outputs.tag }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -50,7 +47,6 @@ jobs:
|
|||||||
git config user.email "actions@gitea.local"
|
git config user.email "actions@gitea.local"
|
||||||
|
|
||||||
- name: Bump patch version
|
- name: Bump patch version
|
||||||
id: bump
|
|
||||||
run: |
|
run: |
|
||||||
CURRENT=$(grep '"version"' package.json | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
|
CURRENT=$(grep '"version"' package.json | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
|
||||||
echo "Current version: ${CURRENT}"
|
echo "Current version: ${CURRENT}"
|
||||||
@@ -68,35 +64,34 @@ jobs:
|
|||||||
sed -i "s/__version__ = \"${CURRENT}\"/__version__ = \"${NEW_VERSION}\"/" version.py
|
sed -i "s/__version__ = \"${CURRENT}\"/__version__ = \"${NEW_VERSION}\"/" version.py
|
||||||
sed -i "s/__version_info__ = .*/__version_info__ = (${MAJOR}, ${MINOR}, ${NEW_PATCH})/" version.py
|
sed -i "s/__version_info__ = .*/__version_info__ = (${MAJOR}, ${MINOR}, ${NEW_PATCH})/" version.py
|
||||||
|
|
||||||
echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
# Write to env file instead of step outputs (avoids act runner bug)
|
||||||
echo "tag=v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
echo "NEW_VERSION=${NEW_VERSION}" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_TAG=v${NEW_VERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Commit and tag
|
- name: Commit and tag
|
||||||
env:
|
env:
|
||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
|
|
||||||
git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml version.py
|
git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml version.py
|
||||||
git commit -m "chore: bump version to ${NEW_VERSION} [skip ci]"
|
git commit -m "chore: bump version to ${NEW_VERSION} [skip ci]"
|
||||||
git tag "v${NEW_VERSION}"
|
git tag "${RELEASE_TAG}"
|
||||||
|
|
||||||
REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|")
|
REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|")
|
||||||
git pull --rebase "${REMOTE_URL}" main || true
|
git pull --rebase "${REMOTE_URL}" main || true
|
||||||
git push "${REMOTE_URL}" HEAD:main
|
git push "${REMOTE_URL}" HEAD:main
|
||||||
git push "${REMOTE_URL}" "v${NEW_VERSION}"
|
git push "${REMOTE_URL}" "${RELEASE_TAG}"
|
||||||
|
|
||||||
- name: Create Gitea release
|
- name: Create Gitea release
|
||||||
env:
|
env:
|
||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
TAG="${{ steps.bump.outputs.tag }}"
|
RELEASE_NAME="Local Transcription ${RELEASE_TAG}"
|
||||||
RELEASE_NAME="Local Transcription ${TAG}"
|
|
||||||
|
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-H "Authorization: token ${BUILD_TOKEN}" \
|
-H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated build.\", \"draft\": false, \"prerelease\": false}" \
|
-d "{\"tag_name\": \"${RELEASE_TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated build.\", \"draft\": false, \"prerelease\": false}" \
|
||||||
"${REPO_API}/releases"
|
"${REPO_API}/releases"
|
||||||
echo "Created release: ${RELEASE_NAME}"
|
echo "Created release: ${RELEASE_NAME}"
|
||||||
|
|
||||||
@@ -105,18 +100,14 @@ jobs:
|
|||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
TAG="${{ steps.bump.outputs.tag }}"
|
|
||||||
|
|
||||||
for workflow in build-app-linux.yml build-app-windows.yml build-app-macos.yml; do
|
for workflow in build-app-linux.yml build-app-windows.yml build-app-macos.yml; do
|
||||||
echo "Dispatching ${workflow} for ${TAG}..."
|
echo "Dispatching ${workflow} for ${RELEASE_TAG}..."
|
||||||
HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/dispatch_resp.txt -X POST \
|
HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/dispatch_resp.txt -X POST \
|
||||||
-H "Authorization: token ${BUILD_TOKEN}" \
|
-H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"ref\": \"main\", \"inputs\": {\"tag\": \"${TAG}\"}}" \
|
-d "{\"ref\": \"main\", \"inputs\": {\"tag\": \"${RELEASE_TAG}\"}}" \
|
||||||
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
||||||
echo " -> HTTP ${HTTP_CODE}"
|
echo " -> HTTP ${HTTP_CODE}"
|
||||||
[ "$HTTP_CODE" != "204" ] && cat /tmp/dispatch_resp.txt && echo ""
|
if [ "$HTTP_CODE" != "204" ]; then cat /tmp/dispatch_resp.txt; echo ""; fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# NOTE: Automatic cleanup disabled -- it races with async builds.
|
|
||||||
# Clean up old releases manually from the Gitea UI when needed.
|
|
||||||
|
|||||||
@@ -27,40 +27,17 @@ jobs:
|
|||||||
needs: test
|
needs: test
|
||||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
version: ${{ steps.bump.outputs.version }}
|
|
||||||
tag: ${{ steps.bump.outputs.tag }}
|
|
||||||
has_changes: ${{ steps.check_changes.outputs.has_changes }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Check for backend changes
|
|
||||||
id: check_changes
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
CHANGED=$(git diff --name-only HEAD~1 HEAD -- client/ server/ backend/ pyproject.toml local-transcription-headless.spec 2>/dev/null || echo "")
|
|
||||||
if [ -n "$CHANGED" ]; then
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Backend changes detected: $CHANGED"
|
|
||||||
else
|
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "No backend changes detected, skipping sidecar build"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Configure git
|
- name: Configure git
|
||||||
if: steps.check_changes.outputs.has_changes == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
git config user.name "Gitea Actions"
|
git config user.name "Gitea Actions"
|
||||||
git config user.email "actions@gitea.local"
|
git config user.email "actions@gitea.local"
|
||||||
|
|
||||||
- name: Bump sidecar patch version
|
- name: Bump sidecar patch version
|
||||||
if: steps.check_changes.outputs.has_changes == 'true'
|
|
||||||
id: bump
|
|
||||||
run: |
|
run: |
|
||||||
CURRENT=$(grep '^version = ' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
CURRENT=$(grep '^version = ' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
|
||||||
echo "Current sidecar version: ${CURRENT}"
|
echo "Current sidecar version: ${CURRENT}"
|
||||||
@@ -74,56 +51,49 @@ jobs:
|
|||||||
|
|
||||||
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" pyproject.toml
|
sed -i "s/^version = \"${CURRENT}\"/version = \"${NEW_VERSION}\"/" pyproject.toml
|
||||||
|
|
||||||
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
|
# Write to env file instead of step outputs (avoids act runner bug)
|
||||||
echo "tag=sidecar-v${NEW_VERSION}" >> $GITHUB_OUTPUT
|
echo "NEW_VERSION=${NEW_VERSION}" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_TAG=sidecar-v${NEW_VERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Commit and tag
|
- name: Commit and tag
|
||||||
if: steps.check_changes.outputs.has_changes == 'true'
|
|
||||||
env:
|
env:
|
||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
NEW_VERSION="${{ steps.bump.outputs.version }}"
|
|
||||||
TAG="${{ steps.bump.outputs.tag }}"
|
|
||||||
git add pyproject.toml
|
git add pyproject.toml
|
||||||
git commit -m "chore: bump sidecar version to ${NEW_VERSION} [skip ci]"
|
git commit -m "chore: bump sidecar version to ${NEW_VERSION} [skip ci]"
|
||||||
git tag "${TAG}"
|
git tag "${RELEASE_TAG}"
|
||||||
|
|
||||||
REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|")
|
REMOTE_URL=$(git remote get-url origin | sed "s|://|://gitea-actions:${BUILD_TOKEN}@|")
|
||||||
git pull --rebase "${REMOTE_URL}" main || true
|
git pull --rebase "${REMOTE_URL}" main || true
|
||||||
git push "${REMOTE_URL}" HEAD:main
|
git push "${REMOTE_URL}" HEAD:main
|
||||||
git push "${REMOTE_URL}" "${TAG}"
|
git push "${REMOTE_URL}" "${RELEASE_TAG}"
|
||||||
|
|
||||||
- name: Create Gitea release
|
- name: Create Gitea release
|
||||||
if: steps.check_changes.outputs.has_changes == 'true'
|
|
||||||
env:
|
env:
|
||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
TAG="${{ steps.bump.outputs.tag }}"
|
RELEASE_NAME="Sidecar v${NEW_VERSION}"
|
||||||
VERSION="${{ steps.bump.outputs.version }}"
|
|
||||||
RELEASE_NAME="Sidecar v${VERSION}"
|
|
||||||
|
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-H "Authorization: token ${BUILD_TOKEN}" \
|
-H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated sidecar build.\", \"draft\": false, \"prerelease\": false}" \
|
-d "{\"tag_name\": \"${RELEASE_TAG}\", \"name\": \"${RELEASE_NAME}\", \"body\": \"Automated sidecar build.\", \"draft\": false, \"prerelease\": false}" \
|
||||||
"${REPO_API}/releases"
|
"${REPO_API}/releases"
|
||||||
echo "Created release: ${RELEASE_NAME}"
|
echo "Created release: ${RELEASE_NAME}"
|
||||||
|
|
||||||
- name: Trigger per-OS sidecar builds
|
- name: Trigger per-OS sidecar builds
|
||||||
if: steps.check_changes.outputs.has_changes == 'true'
|
|
||||||
env:
|
env:
|
||||||
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
BUILD_TOKEN: ${{ secrets.BUILD_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
REPO_API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
TAG="${{ steps.bump.outputs.tag }}"
|
|
||||||
|
|
||||||
for workflow in build-sidecar-linux.yml build-sidecar-windows.yml build-sidecar-macos.yml build-sidecar-cloud.yml; do
|
for workflow in build-sidecar-linux.yml build-sidecar-windows.yml build-sidecar-macos.yml build-sidecar-cloud.yml; do
|
||||||
echo "Dispatching ${workflow} for ${TAG}..."
|
echo "Dispatching ${workflow} for ${RELEASE_TAG}..."
|
||||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||||
-H "Authorization: token ${BUILD_TOKEN}" \
|
-H "Authorization: token ${BUILD_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"ref\": \"main\", \"inputs\": {\"tag\": \"${TAG}\"}}" \
|
-d "{\"ref\": \"main\", \"inputs\": {\"tag\": \"${RELEASE_TAG}\"}}" \
|
||||||
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
"${REPO_API}/actions/workflows/${workflow}/dispatches")
|
||||||
echo " -> HTTP ${HTTP_CODE}"
|
echo " -> HTTP ${HTTP_CODE}"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -151,14 +151,24 @@ class APIServer:
|
|||||||
|
|
||||||
@app.post("/api/start")
|
@app.post("/api/start")
|
||||||
async def start_transcription():
|
async def start_transcription():
|
||||||
success, message = ctrl.start_transcription()
|
import asyncio
|
||||||
|
# Run in thread pool to avoid blocking the event loop
|
||||||
|
# (start_recording can block up to 15s waiting for Deepgram WS)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
success, message = await loop.run_in_executor(
|
||||||
|
None, ctrl.start_transcription
|
||||||
|
)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail=message)
|
raise HTTPException(status_code=400, detail=message)
|
||||||
return {"status": "ok", "message": message}
|
return {"status": "ok", "message": message}
|
||||||
|
|
||||||
@app.post("/api/stop")
|
@app.post("/api/stop")
|
||||||
async def stop_transcription():
|
async def stop_transcription():
|
||||||
success, message = ctrl.stop_transcription()
|
import asyncio
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
success, message = await loop.run_in_executor(
|
||||||
|
None, ctrl.stop_transcription
|
||||||
|
)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail=message)
|
raise HTTPException(status_code=400, detail=message)
|
||||||
return {"status": "ok", "message": message}
|
return {"status": "ok", "message": message}
|
||||||
@@ -223,7 +233,11 @@ class APIServer:
|
|||||||
|
|
||||||
@app.post("/api/reload-engine")
|
@app.post("/api/reload-engine")
|
||||||
async def reload_engine():
|
async def reload_engine():
|
||||||
success, message = ctrl.reload_engine()
|
import asyncio
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
success, message = await loop.run_in_executor(
|
||||||
|
None, ctrl.reload_engine
|
||||||
|
)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=500, detail=message)
|
raise HTTPException(status_code=500, detail=message)
|
||||||
return {"status": "ok", "message": message}
|
return {"status": "ok", "message": message}
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ def test_apply_settings_no_reload_when_same(controller):
|
|||||||
# Ensure config returns the same values
|
# Ensure config returns the same values
|
||||||
controller.config.set("transcription.model", "base.en")
|
controller.config.set("transcription.model", "base.en")
|
||||||
controller.config.set("transcription.device", "auto")
|
controller.config.set("transcription.device", "auto")
|
||||||
|
# Remote mode must also match (no engine means current mode is 'local')
|
||||||
|
controller.config.set("remote.mode", "local")
|
||||||
|
|
||||||
controller.reload_engine = MagicMock(return_value=(True, "reloaded"))
|
controller.reload_engine = MagicMock(return_value=(True, "reloaded"))
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ transcription:
|
|||||||
|
|
||||||
server_sync:
|
server_sync:
|
||||||
enabled: false
|
enabled: false
|
||||||
url: "http://localhost:3000/api/send"
|
url: ""
|
||||||
room: "default"
|
room: "default"
|
||||||
passphrase: ""
|
passphrase: ""
|
||||||
# Font settings are now in the display section (shared for local and server sync)
|
# Font settings are now in the display section (shared for local and server sync)
|
||||||
@@ -69,7 +69,7 @@ web_server:
|
|||||||
host: "127.0.0.1"
|
host: "127.0.0.1"
|
||||||
|
|
||||||
remote:
|
remote:
|
||||||
mode: local # local | managed | byok
|
mode: byok # local | managed | byok
|
||||||
server_url: "" # Proxy server URL for managed mode (e.g., wss://your-proxy.com)
|
server_url: "" # Proxy server URL for managed mode (e.g., wss://your-proxy.com)
|
||||||
auth_token: "" # JWT stored after login (managed mode)
|
auth_token: "" # JWT stored after login (managed mode)
|
||||||
byok_api_key: "" # Deepgram API key for BYOK mode
|
byok_api_key: "" # Deepgram API key for BYOK mode
|
||||||
|
|||||||
@@ -19,9 +19,26 @@ datas = [
|
|||||||
('config/default_config.yaml', 'config'),
|
('config/default_config.yaml', 'config'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Collect sounddevice's bundled PortAudio library (_sounddevice_data)
|
||||||
|
try:
|
||||||
|
import sounddevice
|
||||||
|
sd_path = os.path.dirname(sounddevice.__file__)
|
||||||
|
sd_data = os.path.join(sd_path, '_sounddevice_data')
|
||||||
|
if os.path.exists(sd_data):
|
||||||
|
datas.append((sd_data, '_sounddevice_data'))
|
||||||
|
print(f" + Collected sounddevice PortAudio data from {sd_data}")
|
||||||
|
# Also collect the package itself
|
||||||
|
sd_datas = collect_data_files('sounddevice')
|
||||||
|
if sd_datas:
|
||||||
|
datas += sd_datas
|
||||||
|
print(f" + Collected {len(sd_datas)} sounddevice data files")
|
||||||
|
except ImportError:
|
||||||
|
print(" - Warning: sounddevice not found")
|
||||||
|
|
||||||
# Hidden imports -- only lightweight deps needed for Deepgram streaming
|
# Hidden imports -- only lightweight deps needed for Deepgram streaming
|
||||||
hiddenimports = [
|
hiddenimports = [
|
||||||
'sounddevice',
|
'sounddevice',
|
||||||
|
'_sounddevice_data',
|
||||||
'numpy',
|
'numpy',
|
||||||
# FastAPI and dependencies
|
# FastAPI and dependencies
|
||||||
'fastapi',
|
'fastapi',
|
||||||
|
|||||||
@@ -38,6 +38,21 @@ datas = [
|
|||||||
(vad_assets_path, 'faster_whisper/assets'),
|
(vad_assets_path, 'faster_whisper/assets'),
|
||||||
] + pvporcupine_data_files
|
] + pvporcupine_data_files
|
||||||
|
|
||||||
|
# Collect sounddevice's bundled PortAudio library (_sounddevice_data)
|
||||||
|
try:
|
||||||
|
import sounddevice
|
||||||
|
sd_path = os.path.dirname(sounddevice.__file__)
|
||||||
|
sd_data = os.path.join(sd_path, '_sounddevice_data')
|
||||||
|
if os.path.exists(sd_data):
|
||||||
|
datas.append((sd_data, '_sounddevice_data'))
|
||||||
|
print(f" + Collected sounddevice PortAudio data from {sd_data}")
|
||||||
|
sd_datas = collect_data_files('sounddevice')
|
||||||
|
if sd_datas:
|
||||||
|
datas += sd_datas
|
||||||
|
print(f" + Collected {len(sd_datas)} sounddevice data files")
|
||||||
|
except ImportError:
|
||||||
|
print(" - Warning: sounddevice not found")
|
||||||
|
|
||||||
# Hidden imports -- NO PySide6/Qt needed for headless backend
|
# Hidden imports -- NO PySide6/Qt needed for headless backend
|
||||||
hiddenimports = [
|
hiddenimports = [
|
||||||
# Transcription engine
|
# Transcription engine
|
||||||
@@ -46,6 +61,7 @@ hiddenimports = [
|
|||||||
'faster_whisper.vad',
|
'faster_whisper.vad',
|
||||||
'ctranslate2',
|
'ctranslate2',
|
||||||
'sounddevice',
|
'sounddevice',
|
||||||
|
'_sounddevice_data',
|
||||||
'scipy',
|
'scipy',
|
||||||
'scipy.signal',
|
'scipy.signal',
|
||||||
'numpy',
|
'numpy',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "local-transcription",
|
"name": "local-transcription",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.9",
|
"version": "2.0.15",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "local-transcription"
|
name = "local-transcription"
|
||||||
version = "1.0.8"
|
version = "1.0.11"
|
||||||
description = "A standalone desktop application for real-time speech-to-text transcription using Whisper models"
|
description = "A standalone desktop application for real-time speech-to-text transcription using Whisper models"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -703,6 +703,36 @@ app.post('/api/send', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create room explicitly (no transcription needed)
|
||||||
|
app.post('/api/create-room', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { room, passphrase } = req.body;
|
||||||
|
|
||||||
|
if (!room || !passphrase) {
|
||||||
|
return res.status(400).json({ error: 'Missing room or passphrase' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if room already exists
|
||||||
|
const existing = await loadRoom(room);
|
||||||
|
if (existing) {
|
||||||
|
const valid = await verifyPassphrase(room, passphrase);
|
||||||
|
if (!valid) {
|
||||||
|
return res.status(401).json({ error: 'Room exists with different passphrase' });
|
||||||
|
}
|
||||||
|
return res.json({ status: 'ok', room, created: false, message: 'Room already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the room (verifyPassphrase creates it if it doesn't exist)
|
||||||
|
await verifyPassphrase(room, passphrase);
|
||||||
|
|
||||||
|
console.log(`[Room] Created room "${room}"`);
|
||||||
|
res.json({ status: 'ok', room, created: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in /api/create-room:', err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// List transcriptions
|
// List transcriptions
|
||||||
app.get('/api/list', async (req, res) => {
|
app.get('/api/list', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -1881,7 +1881,7 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "local-transcription"
|
name = "local-transcription"
|
||||||
version = "2.0.3"
|
version = "2.0.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "local-transcription"
|
name = "local-transcription"
|
||||||
version = "2.0.9"
|
version = "2.0.15"
|
||||||
description = "Real-time speech-to-text transcription for streamers"
|
description = "Real-time speech-to-text transcription for streamers"
|
||||||
authors = ["Local Transcription Contributors"]
|
authors = ["Local Transcription Contributors"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -74,14 +74,25 @@ pub fn run() {
|
|||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|app, event| {
|
.run(|app, event| {
|
||||||
if let tauri::RunEvent::Exit = event {
|
match event {
|
||||||
// Stop the sidecar when the app exits
|
tauri::RunEvent::Exit => {
|
||||||
if let Some(state) = app.try_state::<sidecar::ManagedSidecar>() {
|
if let Some(state) = app.try_state::<sidecar::ManagedSidecar>() {
|
||||||
if let Ok(mut mgr) = state.0.lock() {
|
if let Ok(mut mgr) = state.0.lock() {
|
||||||
eprintln!("[app] Stopping sidecar on exit...");
|
eprintln!("[app] Stopping sidecar on exit...");
|
||||||
mgr.stop();
|
mgr.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tauri::RunEvent::ExitRequested { .. } => {
|
||||||
|
// Also stop sidecar on exit request (Cmd+Q on macOS)
|
||||||
|
if let Some(state) = app.try_state::<sidecar::ManagedSidecar>() {
|
||||||
|
if let Ok(mut mgr) = state.0.lock() {
|
||||||
|
eprintln!("[app] Stopping sidecar on exit request...");
|
||||||
|
mgr.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"productName": "Local Transcription",
|
"productName": "Local Transcription",
|
||||||
"version": "2.0.9",
|
"version": "2.0.15",
|
||||||
"identifier": "net.anhonesthost.local-transcription",
|
"identifier": "net.anhonesthost.local-transcription",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
let sidecarState = $state<SidecarState>("checking");
|
let sidecarState = $state<SidecarState>("checking");
|
||||||
let debugLog = $state("");
|
let debugLog = $state("");
|
||||||
let availableUpdate = $state("");
|
let availableUpdate = $state("");
|
||||||
|
let appVersion = $state("");
|
||||||
|
|
||||||
let obsDisplayUrl = $derived(backendStore.obsUrl);
|
let obsDisplayUrl = $derived(backendStore.obsUrl);
|
||||||
let syncDisplayUrl = $derived(backendStore.syncUrl);
|
let syncDisplayUrl = $derived(backendStore.syncUrl);
|
||||||
@@ -108,6 +109,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
// Get app version from Tauri
|
||||||
|
import("@tauri-apps/api/app").then(({ getVersion }) =>
|
||||||
|
getVersion().then((v) => { appVersion = v; })
|
||||||
|
).catch(() => {
|
||||||
|
// Browser dev mode -- read from package.json or use fallback
|
||||||
|
appVersion = "dev";
|
||||||
|
});
|
||||||
|
|
||||||
checkAndLaunchSidecar();
|
checkAndLaunchSidecar();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -201,7 +210,7 @@
|
|||||||
<TranscriptionDisplay />
|
<TranscriptionDisplay />
|
||||||
<Controls />
|
<Controls />
|
||||||
|
|
||||||
<div class="version-label">v{backendStore.version}</div>
|
<div class="version-label">v{appVersion || backendStore.version}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showSettings}
|
{#if showSettings}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { backendStore } from "$lib/stores/backend";
|
import { backendStore } from "$lib/stores/backend";
|
||||||
|
import { configStore } from "$lib/stores/config";
|
||||||
import { transcriptionStore } from "$lib/stores/transcriptions";
|
import { transcriptionStore } from "$lib/stores/transcriptions";
|
||||||
|
|
||||||
let isTranscribing = $derived(backendStore.appState === "transcribing");
|
let isTranscribing = $derived(backendStore.appState === "transcribing");
|
||||||
@@ -8,6 +9,16 @@
|
|||||||
);
|
);
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
|
||||||
|
let remoteMode = $derived(configStore.config.remote.mode);
|
||||||
|
let byokApiKey = $derived(configStore.config.remote.byok_api_key);
|
||||||
|
let authToken = $derived(configStore.config.remote.auth_token);
|
||||||
|
|
||||||
|
let cloudConfigured = $derived(
|
||||||
|
remoteMode === "local" ||
|
||||||
|
(remoteMode === "byok" && byokApiKey.trim() !== "") ||
|
||||||
|
(remoteMode === "managed" && authToken.trim() !== "")
|
||||||
|
);
|
||||||
|
|
||||||
let errorMessage = $state("");
|
let errorMessage = $state("");
|
||||||
|
|
||||||
async function toggleTranscription() {
|
async function toggleTranscription() {
|
||||||
@@ -20,14 +31,17 @@
|
|||||||
} else {
|
} else {
|
||||||
await backendStore.apiPost("/api/start");
|
await backendStore.apiPost("/api/start");
|
||||||
}
|
}
|
||||||
// Poll status to update UI immediately instead of waiting
|
|
||||||
// for WebSocket broadcast (which can be delayed or missed)
|
|
||||||
await backendStore.pollStatus();
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
console.error("Failed to toggle transcription:", msg);
|
// Ignore "Already transcribing/not transcribing" -- just sync the state
|
||||||
errorMessage = msg;
|
if (!msg.includes("400")) {
|
||||||
|
console.error("Failed to toggle transcription:", msg);
|
||||||
|
errorMessage = msg;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
// Always poll status to sync UI with actual backend state,
|
||||||
|
// even if the API call failed (e.g. "Already transcribing")
|
||||||
|
await backendStore.pollStatus();
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +105,7 @@
|
|||||||
<button
|
<button
|
||||||
class={isTranscribing ? "danger" : "primary"}
|
class={isTranscribing ? "danger" : "primary"}
|
||||||
onclick={toggleTranscription}
|
onclick={toggleTranscription}
|
||||||
disabled={!isReady || isLoading}
|
disabled={!isReady || isLoading || !cloudConfigured}
|
||||||
>
|
>
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
...
|
...
|
||||||
@@ -113,6 +127,18 @@
|
|||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<span class="error-msg">{errorMessage}</span>
|
<span class="error-msg">{errorMessage}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if !cloudConfigured && isReady}
|
||||||
|
<div class="cloud-warning">
|
||||||
|
{#if remoteMode === "byok"}
|
||||||
|
<span>API key required. Get one at
|
||||||
|
<a href="https://console.deepgram.com" target="_blank" rel="noopener">console.deepgram.com</a>,
|
||||||
|
then enter it in Settings.</span>
|
||||||
|
{:else if remoteMode === "managed"}
|
||||||
|
<span>Login required. Open Settings to log in.</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -122,6 +148,18 @@
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cloud-warning {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ff9800;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud-warning a {
|
||||||
|
color: #4fc3f7;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -46,6 +46,14 @@
|
|||||||
let managedPassword = $state("");
|
let managedPassword = $state("");
|
||||||
let autoCheckUpdates = $state(true);
|
let autoCheckUpdates = $state(true);
|
||||||
|
|
||||||
|
let isCloudMode = $derived(remoteMode === "managed" || remoteMode === "byok");
|
||||||
|
|
||||||
|
// Room creation / join state
|
||||||
|
let shareCode = $state("");
|
||||||
|
let joinCode = $state("");
|
||||||
|
let roomCreating = $state(false);
|
||||||
|
let roomCreateMessage = $state("");
|
||||||
|
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
let saveMessage = $state("");
|
let saveMessage = $state("");
|
||||||
|
|
||||||
@@ -266,6 +274,110 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CAPTION_SERVER = "https://caption.shadowdao.com";
|
||||||
|
|
||||||
|
function generateRandomName(): string {
|
||||||
|
const adjectives = ['swift', 'bright', 'cosmic', 'electric', 'turbo', 'mega', 'ultra', 'super', 'hyper', 'alpha'];
|
||||||
|
const nouns = ['phoenix', 'dragon', 'tiger', 'falcon', 'comet', 'storm', 'blaze', 'thunder', 'frost', 'nebula'];
|
||||||
|
const num = Math.floor(Math.random() * 10000);
|
||||||
|
return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${nouns[Math.floor(Math.random() * nouns.length)]}-${num}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRandomPassphrase(): string {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeShareCode(url: string, room: string, passphrase: string): string {
|
||||||
|
return btoa(JSON.stringify({ url, room, passphrase }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeShareCode(code: string): { url: string; room: string; passphrase: string } | null {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(atob(code.trim()));
|
||||||
|
if (json.url && json.room && json.passphrase) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateRoom() {
|
||||||
|
roomCreating = true;
|
||||||
|
roomCreateMessage = "";
|
||||||
|
shareCode = "";
|
||||||
|
|
||||||
|
const room = generateRandomName();
|
||||||
|
const passphrase = generateRandomPassphrase();
|
||||||
|
const serverSendUrl = `${CAPTION_SERVER}/api/send`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${CAPTION_SERVER}/api/create-room`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ room, passphrase }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.json().catch(() => ({ error: "Request failed" }));
|
||||||
|
roomCreateMessage = `Error: ${err.error || resp.statusText}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncUrl = serverSendUrl;
|
||||||
|
syncRoom = room;
|
||||||
|
syncPassphrase = passphrase;
|
||||||
|
syncEnabled = true;
|
||||||
|
|
||||||
|
shareCode = encodeShareCode(serverSendUrl, room, passphrase);
|
||||||
|
roomCreateMessage = "Room created! Share the code below with others.";
|
||||||
|
} catch (err) {
|
||||||
|
roomCreateMessage = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
||||||
|
} finally {
|
||||||
|
roomCreating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleJoinRoom() {
|
||||||
|
const decoded = decodeShareCode(joinCode);
|
||||||
|
if (!decoded) {
|
||||||
|
roomCreateMessage = "Invalid share code. Please check and try again.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
syncUrl = decoded.url;
|
||||||
|
syncRoom = decoded.room;
|
||||||
|
syncPassphrase = decoded.passphrase;
|
||||||
|
syncEnabled = true;
|
||||||
|
joinCode = "";
|
||||||
|
roomCreateMessage = "Room joined! Fields have been auto-filled.";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleShareCurrentRoom() {
|
||||||
|
const code = encodeShareCode(syncUrl, syncRoom, syncPassphrase);
|
||||||
|
shareCode = code;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
roomCreateMessage = "Share code copied to clipboard!";
|
||||||
|
} catch {
|
||||||
|
roomCreateMessage = "Share code generated. Copy it from the field below.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyShareCode() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareCode);
|
||||||
|
roomCreateMessage = "Share code copied to clipboard!";
|
||||||
|
} catch {
|
||||||
|
roomCreateMessage = "Failed to copy. Please select and copy manually.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleOverlayClick(e: MouseEvent) {
|
function handleOverlayClick(e: MouseEvent) {
|
||||||
if ((e.target as HTMLElement).classList.contains("settings-overlay")) {
|
if ((e.target as HTMLElement).classList.contains("settings-overlay")) {
|
||||||
handleCancel();
|
handleCancel();
|
||||||
@@ -327,7 +439,90 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Transcription Settings -->
|
<!-- Remote Transcription (moved up for cloud-first UX) -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h3>Transcription Mode</h3>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="remote-mode"
|
||||||
|
value="byok"
|
||||||
|
bind:group={remoteMode}
|
||||||
|
/>
|
||||||
|
Cloud (Deepgram)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="remote-mode"
|
||||||
|
value="managed"
|
||||||
|
bind:group={remoteMode}
|
||||||
|
/>
|
||||||
|
Managed Service
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="remote-mode"
|
||||||
|
value="local"
|
||||||
|
bind:group={remoteMode}
|
||||||
|
/>
|
||||||
|
Local (Whisper)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{#if remoteMode === "byok"}
|
||||||
|
<div class="field">
|
||||||
|
<label for="byok-key">Deepgram API Key</label>
|
||||||
|
<input
|
||||||
|
id="byok-key"
|
||||||
|
type="password"
|
||||||
|
bind:value={byokApiKey}
|
||||||
|
placeholder="Enter your Deepgram API key"
|
||||||
|
/>
|
||||||
|
<p style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">
|
||||||
|
Get a key at <a href="https://console.deepgram.com" target="_blank" rel="noopener" style="color: var(--accent-blue);">console.deepgram.com</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if remoteMode === "managed"}
|
||||||
|
<div class="field">
|
||||||
|
<label for="remote-url">Server URL</label>
|
||||||
|
<input
|
||||||
|
id="remote-url"
|
||||||
|
type="url"
|
||||||
|
bind:value={remoteServerUrl}
|
||||||
|
placeholder="wss://your-proxy.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="managed-auth">
|
||||||
|
<div class="field">
|
||||||
|
<label for="managed-email">Email</label>
|
||||||
|
<input
|
||||||
|
id="managed-email"
|
||||||
|
type="email"
|
||||||
|
bind:value={managedEmail}
|
||||||
|
placeholder="email@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="managed-password">Password</label>
|
||||||
|
<input
|
||||||
|
id="managed-password"
|
||||||
|
type="password"
|
||||||
|
bind:value={managedPassword}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="auth-buttons">
|
||||||
|
<button onclick={handleManagedLogin}>Login</button>
|
||||||
|
<button onclick={handleManagedRegister}>Register</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if !isCloudMode}
|
||||||
|
<!-- Transcription Settings (local Whisper only) -->
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h3>Transcription Settings</h3>
|
<h3>Transcription Settings</h3>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -473,6 +668,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Display Settings -->
|
<!-- Display Settings -->
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
@@ -628,11 +824,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Server Sync -->
|
<!-- Server Sync (Shared Captions) -->
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h3>Server Sync</h3>
|
<h3>Shared Captions</h3>
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
<label for="sync-enabled">Enable Server Sync</label>
|
<label for="sync-enabled">Enable Shared Captions</label>
|
||||||
<input
|
<input
|
||||||
id="sync-enabled"
|
id="sync-enabled"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -640,13 +836,57 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if syncEnabled}
|
{#if syncEnabled}
|
||||||
|
<div class="room-actions">
|
||||||
|
<div class="room-buttons-row">
|
||||||
|
<button
|
||||||
|
onclick={handleCreateRoom}
|
||||||
|
disabled={roomCreating}
|
||||||
|
class="secondary"
|
||||||
|
>
|
||||||
|
{roomCreating ? "Creating..." : "Create Room"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={handleShareCurrentRoom}
|
||||||
|
disabled={!syncUrl.trim() || !syncRoom.trim() || !syncPassphrase.trim()}
|
||||||
|
class="secondary"
|
||||||
|
>
|
||||||
|
Share Current Room
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="join-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={joinCode}
|
||||||
|
placeholder="Paste share code to join"
|
||||||
|
class="join-input"
|
||||||
|
/>
|
||||||
|
<button onclick={handleJoinRoom} disabled={!joinCode.trim()} class="secondary">
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if roomCreateMessage}
|
||||||
|
<p class="room-message" class:error={roomCreateMessage.startsWith("Error")}>{roomCreateMessage}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if shareCode}
|
||||||
|
<div class="share-code-box">
|
||||||
|
<label>Share Code</label>
|
||||||
|
<div class="share-code-row">
|
||||||
|
<input type="text" value={shareCode} readonly class="share-code-input" />
|
||||||
|
<button onclick={copyShareCode} class="secondary">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="sync-url">Server URL</label>
|
<label for="sync-url">Server URL</label>
|
||||||
<input
|
<input
|
||||||
id="sync-url"
|
id="sync-url"
|
||||||
type="url"
|
type="url"
|
||||||
bind:value={syncUrl}
|
bind:value={syncUrl}
|
||||||
placeholder="http://localhost:3000/api/send"
|
placeholder="https://caption.shadowdao.com/api/send"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -664,90 +904,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Remote Transcription -->
|
|
||||||
<section class="settings-section">
|
|
||||||
<h3>Remote Transcription</h3>
|
|
||||||
<div class="radio-group">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="remote-mode"
|
|
||||||
value="local"
|
|
||||||
bind:group={remoteMode}
|
|
||||||
/>
|
|
||||||
Local
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="remote-mode"
|
|
||||||
value="managed"
|
|
||||||
bind:group={remoteMode}
|
|
||||||
/>
|
|
||||||
Managed
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="remote-mode"
|
|
||||||
value="byok"
|
|
||||||
bind:group={remoteMode}
|
|
||||||
/>
|
|
||||||
BYOK (Bring Your Own Key)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{#if remoteMode === "managed"}
|
|
||||||
<div class="field">
|
|
||||||
<label for="remote-url">Server URL</label>
|
|
||||||
<input
|
|
||||||
id="remote-url"
|
|
||||||
type="url"
|
|
||||||
bind:value={remoteServerUrl}
|
|
||||||
placeholder="wss://your-proxy.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if remoteMode === "byok"}
|
|
||||||
<div class="field">
|
|
||||||
<label for="byok-key">Deepgram API Key</label>
|
|
||||||
<input
|
|
||||||
id="byok-key"
|
|
||||||
type="password"
|
|
||||||
bind:value={byokApiKey}
|
|
||||||
placeholder="Enter your Deepgram API key"
|
|
||||||
/>
|
|
||||||
<p style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">
|
|
||||||
Get a key at <a href="https://console.deepgram.com" target="_blank" rel="noopener" style="color: var(--accent-blue);">console.deepgram.com</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if remoteMode === "managed"}
|
|
||||||
<div class="managed-auth">
|
|
||||||
<div class="field">
|
|
||||||
<label for="managed-email">Email</label>
|
|
||||||
<input
|
|
||||||
id="managed-email"
|
|
||||||
type="email"
|
|
||||||
bind:value={managedEmail}
|
|
||||||
placeholder="email@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="managed-password">Password</label>
|
|
||||||
<input
|
|
||||||
id="managed-password"
|
|
||||||
type="password"
|
|
||||||
bind:value={managedPassword}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="auth-buttons">
|
|
||||||
<button onclick={handleManagedLogin}>Login</button>
|
|
||||||
<button onclick={handleManagedRegister}>Register</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Updates -->
|
<!-- Updates -->
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h3>Updates</h3>
|
<h3>Updates</h3>
|
||||||
@@ -943,6 +1099,78 @@
|
|||||||
color: #f44336;
|
color: #f44336;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.room-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-buttons-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-message {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4CAF50;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-message.error {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-code-box {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-code-box label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-code-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-code-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.danger-btn {
|
.danger-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--accent-red, #f44336);
|
border: 1px solid var(--accent-red, #f44336);
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ function getDefaultConfig(): AppConfig {
|
|||||||
},
|
},
|
||||||
server_sync: {
|
server_sync: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
url: "http://localhost:3000/api/send",
|
url: "",
|
||||||
room: "default",
|
room: "default",
|
||||||
passphrase: "",
|
passphrase: "",
|
||||||
},
|
},
|
||||||
@@ -128,7 +128,7 @@ function getDefaultConfig(): AppConfig {
|
|||||||
},
|
},
|
||||||
web_server: { port: 8080, host: "127.0.0.1" },
|
web_server: { port: 8080, host: "127.0.0.1" },
|
||||||
remote: {
|
remote: {
|
||||||
mode: "local",
|
mode: "byok",
|
||||||
server_url: "",
|
server_url: "",
|
||||||
auth_token: "",
|
auth_token: "",
|
||||||
byok_api_key: "",
|
byok_api_key: "",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Version information for Local Transcription."""
|
"""Version information for Local Transcription."""
|
||||||
|
|
||||||
__version__ = "2.0.9"
|
__version__ = "2.0.15"
|
||||||
__version_info__ = (2, 0, 9)
|
__version_info__ = (2, 0, 15)
|
||||||
|
|
||||||
# Version history:
|
# Version history:
|
||||||
# 1.4.0 - Auto-update feature:
|
# 1.4.0 - Auto-update feature:
|
||||||
|
|||||||
Reference in New Issue
Block a user