karaoke-gen 0.75.16__py3-none-any.whl → 0.76.20__py3-none-any.whl
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.
- karaoke_gen/audio_fetcher.py +984 -33
- karaoke_gen/audio_processor.py +4 -0
- karaoke_gen/instrumental_review/static/index.html +37 -14
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +25 -1
- karaoke_gen/karaoke_gen.py +208 -39
- karaoke_gen/lyrics_processor.py +111 -31
- karaoke_gen/utils/__init__.py +26 -0
- karaoke_gen/utils/cli_args.py +15 -6
- karaoke_gen/utils/gen_cli.py +30 -5
- karaoke_gen/utils/remote_cli.py +301 -20
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/METADATA +107 -5
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/RECORD +47 -43
- lyrics_transcriber/core/controller.py +76 -2
- lyrics_transcriber/frontend/index.html +5 -1
- lyrics_transcriber/frontend/package-lock.json +4553 -0
- lyrics_transcriber/frontend/package.json +4 -1
- lyrics_transcriber/frontend/playwright.config.ts +69 -0
- lyrics_transcriber/frontend/public/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/frontend/src/App.tsx +94 -63
- lyrics_transcriber/frontend/src/api.ts +25 -10
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +55 -21
- lyrics_transcriber/frontend/src/components/AppHeader.tsx +65 -0
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +5 -5
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +9 -9
- lyrics_transcriber/frontend/src/components/EditModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +1 -1
- lyrics_transcriber/frontend/src/components/Header.tsx +34 -48
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +22 -21
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +3 -3
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +2 -2
- lyrics_transcriber/frontend/src/components/shared/constants.ts +15 -5
- lyrics_transcriber/frontend/src/main.tsx +1 -7
- lyrics_transcriber/frontend/src/theme.ts +337 -135
- lyrics_transcriber/frontend/vite.config.ts +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-COYImAcx.js → index-BECn1o8Q.js} +38 -22
- lyrics_transcriber/frontend/web_assets/assets/{index-COYImAcx.js.map → index-BECn1o8Q.js.map} +1 -1
- lyrics_transcriber/frontend/web_assets/index.html +1 -1
- lyrics_transcriber/frontend/yarn.lock +1005 -1046
- lyrics_transcriber/output/countdown_processor.py +39 -0
- lyrics_transcriber/review/server.py +1 -1
- lyrics_transcriber/transcribers/audioshake.py +96 -7
- lyrics_transcriber/types.py +14 -12
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "lyrics-transcriber-frontend",
|
|
3
3
|
"private": true,
|
|
4
4
|
"homepage": "https://nomadkaraoke.github.io/lyrics-transcriber-frontend",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.83.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"dev": "vite",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"@mui/icons-material": "^6.3.0",
|
|
20
20
|
"@mui/material": "^6.3.0",
|
|
21
21
|
"@mui/system": "^6.4.3",
|
|
22
|
+
"lucide-react": "^0.562.0",
|
|
22
23
|
"nanoid": "^5.0.9",
|
|
23
24
|
"react": "^18.3.1",
|
|
24
25
|
"react-dom": "^18.3.1",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@eslint/js": "^9.17.0",
|
|
30
|
+
"@playwright/test": "^1.57.0",
|
|
29
31
|
"@types/react": "^18.3.18",
|
|
30
32
|
"@types/react-dom": "^18.3.5",
|
|
31
33
|
"@vitejs/plugin-react": "^4.3.4",
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
"eslint-plugin-react-refresh": "^0.4.16",
|
|
35
37
|
"gh-pages": "^6.3.0",
|
|
36
38
|
"globals": "^15.14.0",
|
|
39
|
+
"playwright": "^1.57.0",
|
|
37
40
|
"typescript": "~5.6.2",
|
|
38
41
|
"typescript-eslint": "^8.18.2",
|
|
39
42
|
"vite": "^6.0.5"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Playwright configuration for lyrics-transcriber frontend E2E tests.
|
|
5
|
+
* Tests run against the local dev server (localhost:5173) served by vite.
|
|
6
|
+
*/
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
testDir: './e2e',
|
|
9
|
+
|
|
10
|
+
// Run tests sequentially for now
|
|
11
|
+
fullyParallel: false,
|
|
12
|
+
|
|
13
|
+
// Fail the build on CI if you accidentally left test.only in the source code
|
|
14
|
+
forbidOnly: !!process.env.CI,
|
|
15
|
+
|
|
16
|
+
// Retry on CI only
|
|
17
|
+
retries: process.env.CI ? 2 : 0,
|
|
18
|
+
|
|
19
|
+
// Single worker
|
|
20
|
+
workers: 1,
|
|
21
|
+
|
|
22
|
+
// Reporter to use
|
|
23
|
+
reporter: [
|
|
24
|
+
['html', { open: 'never' }],
|
|
25
|
+
['list'],
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
// Shared settings for all tests
|
|
29
|
+
use: {
|
|
30
|
+
// Base URL for the local dev server
|
|
31
|
+
baseURL: 'http://localhost:5173',
|
|
32
|
+
|
|
33
|
+
// Collect trace on failure for debugging
|
|
34
|
+
trace: 'on-first-retry',
|
|
35
|
+
|
|
36
|
+
// Screenshot on failure
|
|
37
|
+
screenshot: 'only-on-failure',
|
|
38
|
+
|
|
39
|
+
// Video on failure
|
|
40
|
+
video: 'on-first-retry',
|
|
41
|
+
|
|
42
|
+
// Increase timeout for complex interactions
|
|
43
|
+
actionTimeout: 30000,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Longer timeout for tests
|
|
47
|
+
timeout: 120000, // 2 minutes per test
|
|
48
|
+
|
|
49
|
+
// Expect timeout for assertions
|
|
50
|
+
expect: {
|
|
51
|
+
timeout: 30000, // 30 seconds for expects
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Configure projects (browsers)
|
|
55
|
+
projects: [
|
|
56
|
+
{
|
|
57
|
+
name: 'chromium',
|
|
58
|
+
use: { ...devices['Desktop Chrome'] },
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
|
|
62
|
+
// Run local dev server before starting tests
|
|
63
|
+
webServer: {
|
|
64
|
+
command: 'yarn dev',
|
|
65
|
+
url: 'http://localhost:5173',
|
|
66
|
+
reuseExistingServer: !process.env.CI,
|
|
67
|
+
timeout: 120000,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="45 60 255 135">
|
|
4
|
+
<g data-v-70b83f88="" fill="#ff7acc" class="basesvg" transform="translate(48.368003845214844,62.62239074707031)"><g fill-rule="" class="tp-name" transform="translate(0,0)"><g transform="scale(1.4000000000000004)"><g stroke="#ff7acc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" paint-order="stroke" data-gra-attr="stroke" fill-opacity="0"><path d="M1.4-33.44L1.4 0C31.48 0 3.96 0 34.04 0L34.04-32.64 17.81-32.64 17.81-20.61 1.4-33.44ZM52.21-33.76C42.6-33.76 34.81-25.97 34.81-16.37 34.81-6.76 42.6 1.03 52.21 1.03 61.81 1.03 69.6-6.76 69.6-16.37 69.6-25.97 61.81-33.76 52.21-33.76ZM70.7 0L103.34 0 103.25-33.9 86.93-21.54 70.61-33.9 70.7 0ZM102.34 0C138.2 0 107.52 0 143.56 0L122.95-34.93 102.34 0ZM158.84 0.05C167.84 0.05 175.16-7.27 175.16-16.27 175.16-25.27 167.84-32.64 158.84-32.64L142.52-32.64C142.52 0.75 142.52-33.34 142.52 0.05L158.84 0.05Z" transform="translate(-1.399999976158142, 34.93000030517578)"></path></g><g transform="translate(0,38.959999084472656)"><g stroke="#ff7acc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" paint-order="stroke" data-gra-attr="stroke" fill="#ff7acc" fill-opacity="0" transform="scale(1.02719)"><path d="M24.43 0L17.67 0 7.98-11.87 7.98 0 2.63 0 2.63-26.64 7.98-26.64 7.98-14.69 17.67-26.64 24.12-26.64 13.13-13.44 24.43 0ZM45.03 0L43.27-5.08 32.66-5.08 30.91 0 25.3 0 34.88-26.68 41.1-26.68 50.68 0 45.03 0ZM34.11-9.35L41.82-9.35 37.97-20.5 34.11-9.35ZM73.19 0L67.01 0 61.13-10.38 58.61-10.38 58.61 0 53.27 0 53.27-26.64 63.27-26.64Q66.36-26.64 68.53-25.55 70.71-24.47 71.8-22.61 72.89-20.76 72.89-18.47L72.89-18.47Q72.89-15.84 71.36-13.72 69.83-11.6 66.82-10.8L66.82-10.8 73.19 0ZM58.61-22.21L58.61-14.39 63.08-14.39Q65.25-14.39 66.32-15.44 67.39-16.49 67.39-18.36L67.39-18.36Q67.39-20.19 66.32-21.2 65.25-22.21 63.08-22.21L63.08-22.21 58.61-22.21ZM94.82 0L93.07-5.08 82.46-5.08 80.7 0 75.09 0 84.67-26.68 90.89-26.68 100.47 0 94.82 0ZM83.91-9.35L91.62-9.35 87.76-20.5 83.91-9.35ZM115.43 0.27Q111.69 0.27 108.56-1.49 105.43-3.24 103.6-6.35 101.76-9.47 101.76-13.4L101.76-13.4Q101.76-17.29 103.6-20.4 105.43-23.51 108.56-25.27 111.69-27.02 115.43-27.02L115.43-27.02Q119.21-27.02 122.32-25.27 125.43-23.51 127.24-20.4 129.05-17.29 129.05-13.4L129.05-13.4Q129.05-9.47 127.24-6.35 125.43-3.24 122.3-1.49 119.17 0.27 115.43 0.27L115.43 0.27ZM115.43-4.5Q117.83-4.5 119.66-5.59 121.5-6.68 122.53-8.7 123.56-10.73 123.56-13.4L123.56-13.4Q123.56-16.07 122.53-18.07 121.5-20.08 119.66-21.15 117.83-22.21 115.43-22.21L115.43-22.21Q113.02-22.21 111.17-21.15 109.32-20.08 108.29-18.07 107.26-16.07 107.26-13.4L107.26-13.4Q107.26-10.73 108.29-8.7 109.32-6.68 111.17-5.59 113.02-4.5 115.43-4.5L115.43-4.5ZM153.82 0L147.06 0 137.37-11.87 137.37 0 132.02 0 132.02-26.64 137.37-26.64 137.37-14.69 147.06-26.64 153.51-26.64 142.52-13.44 153.82 0ZM171.79-22.33L161.67-22.33 161.67-15.65 170.64-15.65 170.64-11.41 161.67-11.41 161.67-4.35 171.79-4.35 171.79 0 156.33 0 156.33-26.68 171.79-26.68 171.79-22.33Z" transform="translate(-2.630000114440918, 27.020000457763672)"></path></g></g></g></g> <g data-gra="path-slogan" fill-rule="" class="tp-slogan" fill="#ffdf6b" transform="translate(5,111.788818359375)"><rect x="0" height="1" y="5.9832000732421875" width="14.278396606445312"></rect> <rect height="1" y="5.9832000732421875" width="14.278396606445312" x="218.985595703125"></rect> <g transform="translate(17.278396606445312,0)"><g transform="scale(1.2800000000000002)"><path d="M10.21-8.38L12.02-8.38L9.68 0L7.70 0L6.13-5.96L4.49 0L2.52 0.01L0.26-8.38L2.06-8.38L3.54-1.87L5.24-8.38L7.12-8.38L8.72-1.91L10.21-8.38ZM18.38-8.38L20.06-8.38L20.06 0L18.38 0L18.38-3.56L14.80-3.56L14.80 0L13.12 0L13.12-8.38L14.80-8.38L14.80-4.93L18.38-4.93L18.38-8.38ZM26.58-7.02L23.40-7.02L23.40-4.92L26.22-4.92L26.22-3.59L23.40-3.59L23.40-1.37L26.58-1.37L26.58 0L21.72 0L21.72-8.39L26.58-8.39L26.58-7.02ZM34.37 0L32.42 0L30.58-3.26L29.78-3.26L29.78 0L28.10 0L28.10-8.38L31.25-8.38Q32.22-8.38 32.90-8.03Q33.59-7.69 33.93-7.11Q34.27-6.53 34.27-5.81L34.27-5.81Q34.27-4.98 33.79-4.31Q33.31-3.65 32.36-3.40L32.36-3.40L34.37 0ZM29.78-6.98L29.78-4.52L31.19-4.52Q31.87-4.52 32.21-4.85Q32.54-5.18 32.54-5.77L32.54-5.77Q32.54-6.35 32.21-6.67Q31.87-6.98 31.19-6.98L31.19-6.98L29.78-6.98ZM40.66-7.02L37.48-7.02L37.48-4.92L40.30-4.92L40.30-3.59L37.48-3.59L37.48-1.37L40.66-1.37L40.66 0L35.80 0L35.80-8.39L40.66-8.39L40.66-7.02ZM47.92-8.38L49.70-8.38L46.63 0L44.59 0L41.52-8.38L43.32-8.38L45.62-1.72L47.92-8.38ZM55.57-7.02L52.39-7.02L52.39-4.92L55.21-4.92L55.21-3.59L52.39-3.59L52.39-1.37L55.57-1.37L55.57 0L50.71 0L50.71-8.39L55.57-8.39L55.57-7.02ZM63.36 0L61.42 0L59.57-3.26L58.78-3.26L58.78 0L57.10 0L57.10-8.38L60.24-8.38Q61.21-8.38 61.90-8.03Q62.58-7.69 62.92-7.11Q63.26-6.53 63.26-5.81L63.26-5.81Q63.26-4.98 62.78-4.31Q62.30-3.65 61.36-3.40L61.36-3.40L63.36 0ZM58.78-6.98L58.78-4.52L60.18-4.52Q60.86-4.52 61.20-4.85Q61.54-5.18 61.54-5.77L61.54-5.77Q61.54-6.35 61.20-6.67Q60.86-6.98 60.18-6.98L60.18-6.98L58.78-6.98ZM72.43-8.38L74.30-8.38L71.47-2.92L71.47 0L69.79 0L69.79-2.92L66.95-8.38L68.84-8.38L70.64-4.55L72.43-8.38ZM79.16 0.08Q77.99 0.08 77.00-0.47Q76.02-1.02 75.44-2.00Q74.87-2.98 74.87-4.21L74.87-4.21Q74.87-5.44 75.44-6.41Q76.02-7.39 77.00-7.94Q77.99-8.50 79.16-8.50L79.16-8.50Q80.35-8.50 81.33-7.94Q82.31-7.39 82.88-6.41Q83.45-5.44 83.45-4.21L83.45-4.21Q83.45-2.98 82.88-2.00Q82.31-1.02 81.32-0.47Q80.34 0.08 79.16 0.08L79.16 0.08ZM79.16-1.42Q79.92-1.42 80.50-1.76Q81.07-2.10 81.40-2.74Q81.72-3.37 81.72-4.21L81.72-4.21Q81.72-5.05 81.40-5.68Q81.07-6.31 80.50-6.65Q79.92-6.98 79.16-6.98L79.16-6.98Q78.41-6.98 77.83-6.65Q77.24-6.31 76.92-5.68Q76.60-5.05 76.60-4.21L76.60-4.21Q76.60-3.37 76.92-2.74Q77.24-2.10 77.83-1.76Q78.41-1.42 79.16-1.42L79.16-1.42ZM84.67-8.38L86.35-8.38L86.35-3.19Q86.35-2.34 86.80-1.89Q87.24-1.44 88.04-1.44L88.04-1.44Q88.86-1.44 89.30-1.89Q89.75-2.34 89.75-3.19L89.75-3.19L89.75-8.38L91.44-8.38L91.44-3.20Q91.44-2.14 90.98-1.40Q90.52-0.66 89.74-0.29Q88.97 0.08 88.02 0.08L88.02 0.08Q87.08 0.08 86.32-0.29Q85.56-0.66 85.12-1.40Q84.67-2.14 84.67-3.20L84.67-3.20L84.67-8.38ZM101.60 0L101.05-1.60L97.72-1.60L97.16 0L95.40 0L98.41-8.39L100.37-8.39L103.38 0L101.60 0ZM98.17-2.94L100.60-2.94L99.38-6.44L98.17-2.94ZM110.77 0L108.83 0L106.98-3.26L106.19-3.26L106.19 0L104.51 0L104.51-8.38L107.65-8.38Q108.62-8.38 109.31-8.03Q109.99-7.69 110.33-7.11Q110.68-6.53 110.68-5.81L110.68-5.81Q110.68-4.98 110.20-4.31Q109.72-3.65 108.77-3.40L108.77-3.40L110.77 0ZM106.19-6.98L106.19-4.52L107.59-4.52Q108.28-4.52 108.61-4.85Q108.95-5.18 108.95-5.77L108.95-5.77Q108.95-6.35 108.61-6.67Q108.28-6.98 107.59-6.98L107.59-6.98L106.19-6.98ZM117.06-7.02L113.88-7.02L113.88-4.92L116.70-4.92L116.70-3.59L113.88-3.59L113.88-1.37L117.06-1.37L117.06 0L112.20 0L112.20-8.39L117.06-8.39L117.06-7.02ZM118.72-1.76L120.42-1.76L119.02 1.61L117.94 1.61L118.72-1.76ZM127.38 0.08Q126.50 0.08 125.80-0.22Q125.10-0.52 124.69-1.08Q124.28-1.64 124.27-2.41L124.27-2.41L126.07-2.41Q126.11-1.90 126.44-1.60Q126.77-1.30 127.34-1.30L127.34-1.30Q127.93-1.30 128.27-1.58Q128.60-1.86 128.60-2.32L128.60-2.32Q128.60-2.69 128.38-2.93Q128.15-3.17 127.81-3.31Q127.46-3.44 126.86-3.61L126.86-3.61Q126.05-3.85 125.54-4.09Q125.03-4.32 124.66-4.79Q124.30-5.27 124.30-6.06L124.30-6.06Q124.30-6.80 124.67-7.36Q125.04-7.91 125.71-8.20Q126.38-8.50 127.25-8.50L127.25-8.50Q128.54-8.50 129.35-7.87Q130.16-7.24 130.25-6.11L130.25-6.11L128.40-6.11Q128.38-6.54 128.03-6.82Q127.69-7.10 127.13-7.10L127.13-7.10Q126.64-7.10 126.34-6.85Q126.05-6.60 126.05-6.12L126.05-6.12Q126.05-5.78 126.27-5.56Q126.49-5.34 126.82-5.20Q127.15-5.06 127.75-4.88L127.75-4.88Q128.57-4.64 129.08-4.40Q129.60-4.16 129.97-3.68Q130.34-3.20 130.34-2.42L130.34-2.42Q130.34-1.75 130.00-1.18Q129.65-0.60 128.98-0.26Q128.30 0.08 127.38 0.08L127.38 0.08ZM131.80-8.38L133.48-8.38L133.48 0L131.80 0L131.80-8.38ZM142.30-8.39L142.30 0L140.62 0L136.81-5.75L136.81 0L135.13 0L135.13-8.39L136.81-8.39L140.62-2.63L140.62-8.39L142.30-8.39ZM151.72-5.86L149.78-5.86Q149.50-6.38 148.99-6.66Q148.49-6.94 147.82-6.94L147.82-6.94Q147.07-6.94 146.50-6.60Q145.92-6.26 145.60-5.64Q145.27-5.02 145.27-4.20L145.27-4.20Q145.27-3.36 145.60-2.74Q145.93-2.11 146.52-1.78Q147.11-1.44 147.89-1.44L147.89-1.44Q148.85-1.44 149.46-1.95Q150.07-2.46 150.26-3.37L150.26-3.37L147.38-3.37L147.38-4.66L151.92-4.66L151.92-3.19Q151.75-2.32 151.20-1.57Q150.65-0.83 149.78-0.38Q148.91 0.07 147.83 0.07L147.83 0.07Q146.62 0.07 145.64-0.47Q144.66-1.02 144.10-1.99Q143.54-2.96 143.54-4.20L143.54-4.20Q143.54-5.44 144.10-6.41Q144.66-7.39 145.64-7.94Q146.62-8.48 147.82-8.48L147.82-8.48Q149.23-8.48 150.28-7.79Q151.32-7.10 151.72-5.86L151.72-5.86ZM153.53-8.52L155.34-8.52L155.15-2.72L153.73-2.72L153.53-8.52ZM154.48 0.08Q154.02 0.08 153.73-0.20Q153.43-0.48 153.43-0.90L153.43-0.90Q153.43-1.32 153.73-1.60Q154.02-1.88 154.48-1.88L154.48-1.88Q154.92-1.88 155.21-1.60Q155.50-1.32 155.50-0.90L155.50-0.90Q155.50-0.48 155.21-0.20Q154.92 0.08 154.48 0.08L154.48 0.08Z" transform="translate(-0.264, 8.52)"></path></g></g></g></g>
|
|
5
|
+
</svg>
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
|
2
2
|
import { Alert, Box, Button, Modal, Typography } from '@mui/material'
|
|
3
|
-
import {
|
|
3
|
+
import { ThemeProvider } from '@mui/material/styles'
|
|
4
|
+
import CssBaseline from '@mui/material/CssBaseline'
|
|
5
|
+
import { useEffect, useState, useMemo } from 'react'
|
|
4
6
|
import { ApiClient, FileOnlyClient, LiveApiClient } from './api'
|
|
5
7
|
import CorrectionMetrics from './components/CorrectionMetrics'
|
|
6
8
|
import LyricsAnalyzer from './components/LyricsAnalyzer'
|
|
9
|
+
import AppHeader from './components/AppHeader'
|
|
7
10
|
import { CorrectionData } from './types'
|
|
11
|
+
import { createAppTheme } from './theme'
|
|
12
|
+
|
|
13
|
+
const THEME_STORAGE_KEY = 'nomad-karaoke-lyrics-theme'
|
|
8
14
|
|
|
9
15
|
export default function App() {
|
|
16
|
+
const [isDarkMode, setIsDarkMode] = useState(() => {
|
|
17
|
+
// Initialize from localStorage
|
|
18
|
+
const stored = localStorage.getItem(THEME_STORAGE_KEY)
|
|
19
|
+
return stored !== 'light' // Default to dark
|
|
20
|
+
})
|
|
10
21
|
const [data, setData] = useState<CorrectionData | null>(null)
|
|
11
22
|
const [showMetadata, setShowMetadata] = useState(false)
|
|
12
23
|
const [error, setError] = useState<string | null>(null)
|
|
@@ -14,30 +25,42 @@ export default function App() {
|
|
|
14
25
|
const [isReadOnly, setIsReadOnly] = useState(true)
|
|
15
26
|
const [audioHash, setAudioHash] = useState<string>('')
|
|
16
27
|
|
|
28
|
+
// Create theme based on mode
|
|
29
|
+
const theme = useMemo(() => createAppTheme(isDarkMode ? 'dark' : 'light'), [isDarkMode])
|
|
30
|
+
|
|
31
|
+
// Handle theme toggle
|
|
32
|
+
const handleToggleTheme = () => {
|
|
33
|
+
const newIsDark = !isDarkMode
|
|
34
|
+
setIsDarkMode(newIsDark)
|
|
35
|
+
localStorage.setItem(THEME_STORAGE_KEY, newIsDark ? 'dark' : 'light')
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
useEffect(() => {
|
|
18
39
|
// Parse query parameters
|
|
19
40
|
const params = new URLSearchParams(window.location.search)
|
|
20
41
|
const encodedApiUrl = params.get('baseApiUrl')
|
|
21
42
|
const audioHashParam = params.get('audioHash')
|
|
43
|
+
const reviewTokenParam = params.get('reviewToken')
|
|
22
44
|
|
|
23
45
|
if (encodedApiUrl) {
|
|
24
46
|
const baseApiUrl = decodeURIComponent(encodedApiUrl)
|
|
25
|
-
|
|
47
|
+
// Pass reviewToken to LiveApiClient for authentication
|
|
48
|
+
setApiClient(new LiveApiClient(baseApiUrl, reviewTokenParam || undefined))
|
|
26
49
|
setIsReadOnly(false)
|
|
27
50
|
if (audioHashParam) {
|
|
28
51
|
setAudioHash(audioHashParam)
|
|
29
52
|
}
|
|
30
53
|
// Fetch initial data
|
|
31
|
-
fetchData(baseApiUrl)
|
|
54
|
+
fetchData(baseApiUrl, reviewTokenParam || undefined)
|
|
32
55
|
} else {
|
|
33
56
|
setApiClient(new FileOnlyClient())
|
|
34
57
|
setIsReadOnly(true)
|
|
35
58
|
}
|
|
36
59
|
}, [])
|
|
37
60
|
|
|
38
|
-
const fetchData = async (baseUrl: string) => {
|
|
61
|
+
const fetchData = async (baseUrl: string, reviewToken?: string) => {
|
|
39
62
|
try {
|
|
40
|
-
const client = new LiveApiClient(baseUrl)
|
|
63
|
+
const client = new LiveApiClient(baseUrl, reviewToken)
|
|
41
64
|
const data = await client.getCorrectionData()
|
|
42
65
|
// console.log('Full correction data from API:', data)
|
|
43
66
|
setData(data)
|
|
@@ -143,70 +166,78 @@ export default function App() {
|
|
|
143
166
|
|
|
144
167
|
if (!data) {
|
|
145
168
|
return (
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<>
|
|
154
|
-
<Alert severity="info" sx={{ mb: 2 }}>
|
|
155
|
-
Running in read-only mode. Connect to an API to enable editing.
|
|
169
|
+
<ThemeProvider theme={theme}>
|
|
170
|
+
<CssBaseline />
|
|
171
|
+
<AppHeader isDarkMode={isDarkMode} onToggleTheme={handleToggleTheme} />
|
|
172
|
+
<Box sx={{ p: 3 }}>
|
|
173
|
+
{error && (
|
|
174
|
+
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
|
|
175
|
+
{error}
|
|
156
176
|
</Alert>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
177
|
+
)}
|
|
178
|
+
{isReadOnly ? (
|
|
179
|
+
<>
|
|
180
|
+
<Alert severity="info" sx={{ mb: 2 }}>
|
|
181
|
+
Running in read-only mode. Connect to an API to enable editing.
|
|
182
|
+
</Alert>
|
|
183
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
|
184
|
+
<Typography variant="h4">
|
|
185
|
+
Lyrics Correction Review
|
|
186
|
+
</Typography>
|
|
187
|
+
<Button
|
|
188
|
+
variant="outlined"
|
|
189
|
+
startIcon={<UploadFileIcon />}
|
|
190
|
+
onClick={handleFileLoad}
|
|
191
|
+
>
|
|
192
|
+
Load File
|
|
193
|
+
</Button>
|
|
194
|
+
</Box>
|
|
195
|
+
<Box sx={{ mb: 3 }}>
|
|
196
|
+
<CorrectionMetrics />
|
|
197
|
+
</Box>
|
|
198
|
+
</>
|
|
199
|
+
) : (
|
|
200
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
|
|
201
|
+
<Typography variant="h6" color="text.secondary">
|
|
202
|
+
Loading Lyrics Transcription Review...
|
|
160
203
|
</Typography>
|
|
161
|
-
<Button
|
|
162
|
-
variant="outlined"
|
|
163
|
-
startIcon={<UploadFileIcon />}
|
|
164
|
-
onClick={handleFileLoad}
|
|
165
|
-
>
|
|
166
|
-
Load File
|
|
167
|
-
</Button>
|
|
168
|
-
</Box>
|
|
169
|
-
<Box sx={{ mb: 3 }}>
|
|
170
|
-
<CorrectionMetrics />
|
|
171
204
|
</Box>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<Typography variant="h6" color="text.secondary">
|
|
176
|
-
Loading Lyrics Correction Review...
|
|
177
|
-
</Typography>
|
|
178
|
-
</Box>
|
|
179
|
-
)}
|
|
180
|
-
</Box>
|
|
205
|
+
)}
|
|
206
|
+
</Box>
|
|
207
|
+
</ThemeProvider>
|
|
181
208
|
)
|
|
182
209
|
}
|
|
183
210
|
|
|
184
211
|
return (
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
<ThemeProvider theme={theme}>
|
|
213
|
+
<CssBaseline />
|
|
214
|
+
<AppHeader isDarkMode={isDarkMode} onToggleTheme={handleToggleTheme} />
|
|
215
|
+
<Box sx={{
|
|
216
|
+
p: 1.5,
|
|
217
|
+
pb: 3,
|
|
218
|
+
maxWidth: '100%',
|
|
219
|
+
overflowX: 'hidden'
|
|
220
|
+
}}>
|
|
221
|
+
{error && (
|
|
222
|
+
<Alert severity="error" sx={{ mb: 1 }} onClose={() => setError(null)}>
|
|
223
|
+
{error}
|
|
224
|
+
</Alert>
|
|
225
|
+
)}
|
|
226
|
+
{isReadOnly && (
|
|
227
|
+
<Alert severity="info" sx={{ mb: 1 }}>
|
|
228
|
+
Running in read-only mode. Connect to an API to enable editing.
|
|
229
|
+
</Alert>
|
|
230
|
+
)}
|
|
231
|
+
<LyricsAnalyzer
|
|
232
|
+
data={data}
|
|
233
|
+
onFileLoad={handleFileLoad}
|
|
234
|
+
onShowMetadata={() => setShowMetadata(true)}
|
|
235
|
+
apiClient={apiClient}
|
|
236
|
+
isReadOnly={isReadOnly}
|
|
237
|
+
audioHash={audioHash}
|
|
238
|
+
/>
|
|
239
|
+
{renderMetadataModal()}
|
|
240
|
+
</Box>
|
|
241
|
+
</ThemeProvider>
|
|
211
242
|
)
|
|
212
243
|
}
|
|
@@ -35,14 +35,29 @@ interface AddLyricsRequest {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export class LiveApiClient implements ApiClient {
|
|
38
|
-
|
|
38
|
+
private reviewToken?: string;
|
|
39
|
+
|
|
40
|
+
constructor(private baseUrl: string, reviewToken?: string) {
|
|
39
41
|
this.baseUrl = baseUrl.replace(/\/$/, '')
|
|
42
|
+
this.reviewToken = reviewToken
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
public isUpdatingHandlers = false;
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Build URL with reviewToken query parameter if available
|
|
49
|
+
*/
|
|
50
|
+
private buildUrl(path: string): string {
|
|
51
|
+
const url = `${this.baseUrl}${path}`
|
|
52
|
+
if (this.reviewToken) {
|
|
53
|
+
const separator = url.includes('?') ? '&' : '?'
|
|
54
|
+
return `${url}${separator}review_token=${encodeURIComponent(this.reviewToken)}`
|
|
55
|
+
}
|
|
56
|
+
return url
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
async getCorrectionData(): Promise<CorrectionData> {
|
|
45
|
-
const response = await fetch(
|
|
60
|
+
const response = await fetch(this.buildUrl('/correction-data'));
|
|
46
61
|
if (!response.ok) {
|
|
47
62
|
throw new Error(`API error: ${response.statusText}`);
|
|
48
63
|
}
|
|
@@ -64,7 +79,7 @@ export class LiveApiClient implements ApiClient {
|
|
|
64
79
|
corrected_segments: data.corrected_segments
|
|
65
80
|
};
|
|
66
81
|
|
|
67
|
-
const response = await fetch(
|
|
82
|
+
const response = await fetch(this.buildUrl('/complete'), {
|
|
68
83
|
method: 'POST',
|
|
69
84
|
headers: {
|
|
70
85
|
'Content-Type': 'application/json',
|
|
@@ -78,7 +93,7 @@ export class LiveApiClient implements ApiClient {
|
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
getAudioUrl(audioHash: string): string {
|
|
81
|
-
return
|
|
96
|
+
return this.buildUrl(`/audio/${audioHash}`)
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
async generatePreviewVideo(data: CorrectionData): Promise<PreviewVideoResponse> {
|
|
@@ -88,7 +103,7 @@ export class LiveApiClient implements ApiClient {
|
|
|
88
103
|
corrected_segments: data.corrected_segments
|
|
89
104
|
};
|
|
90
105
|
|
|
91
|
-
const response = await fetch(
|
|
106
|
+
const response = await fetch(this.buildUrl('/preview-video'), {
|
|
92
107
|
method: 'POST',
|
|
93
108
|
headers: {
|
|
94
109
|
'Content-Type': 'application/json',
|
|
@@ -107,7 +122,7 @@ export class LiveApiClient implements ApiClient {
|
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
getPreviewVideoUrl(previewHash: string): string {
|
|
110
|
-
return
|
|
125
|
+
return this.buildUrl(`/preview-video/${previewHash}`);
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
async updateHandlers(enabledHandlers: string[]): Promise<CorrectionData> {
|
|
@@ -116,7 +131,7 @@ export class LiveApiClient implements ApiClient {
|
|
|
116
131
|
console.log('API: Set isUpdatingHandlers to', this.isUpdatingHandlers);
|
|
117
132
|
|
|
118
133
|
try {
|
|
119
|
-
const response = await fetch(
|
|
134
|
+
const response = await fetch(this.buildUrl('/handlers'), {
|
|
120
135
|
method: 'POST',
|
|
121
136
|
headers: {
|
|
122
137
|
'Content-Type': 'application/json',
|
|
@@ -147,7 +162,7 @@ export class LiveApiClient implements ApiClient {
|
|
|
147
162
|
lyrics
|
|
148
163
|
};
|
|
149
164
|
|
|
150
|
-
const response = await fetch(
|
|
165
|
+
const response = await fetch(this.buildUrl('/add-lyrics'), {
|
|
151
166
|
method: 'POST',
|
|
152
167
|
headers: {
|
|
153
168
|
'Content-Type': 'application/json',
|
|
@@ -170,7 +185,7 @@ export class LiveApiClient implements ApiClient {
|
|
|
170
185
|
async submitAnnotations(annotations: Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>[]): Promise<void> {
|
|
171
186
|
// Submit each annotation to the backend
|
|
172
187
|
for (const annotation of annotations) {
|
|
173
|
-
const response = await fetch(
|
|
188
|
+
const response = await fetch(this.buildUrl('/v1/annotations'), {
|
|
174
189
|
method: 'POST',
|
|
175
190
|
headers: {
|
|
176
191
|
'Content-Type': 'application/json',
|
|
@@ -186,7 +201,7 @@ export class LiveApiClient implements ApiClient {
|
|
|
186
201
|
}
|
|
187
202
|
|
|
188
203
|
async getAnnotationStats(): Promise<any> {
|
|
189
|
-
const response = await fetch(
|
|
204
|
+
const response = await fetch(this.buildUrl('/v1/annotations/stats'));
|
|
190
205
|
if (!response.ok) {
|
|
191
206
|
throw new Error(`API error: ${response.statusText}`);
|
|
192
207
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
type Props = {
|
|
4
4
|
isOpen: boolean;
|
|
@@ -12,22 +12,57 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
|
|
|
12
12
|
const [finalText, setFinalText] = React.useState("");
|
|
13
13
|
const [reasonCategory, setReason] = React.useState("AI_CORRECT");
|
|
14
14
|
const [reasonDetail, setDetail] = React.useState("");
|
|
15
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
16
|
+
|
|
17
|
+
// Handle Escape key to close modal
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
20
|
+
if (e.key === 'Escape' && isOpen) {
|
|
21
|
+
onClose();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
25
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
26
|
+
}, [isOpen, onClose]);
|
|
27
|
+
|
|
28
|
+
// Focus modal on open
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (isOpen && modalRef.current) {
|
|
31
|
+
modalRef.current.focus();
|
|
32
|
+
}
|
|
33
|
+
}, [isOpen]);
|
|
15
34
|
|
|
16
35
|
if (!isOpen) return null;
|
|
17
36
|
|
|
37
|
+
// Dark theme colors matching karaoke-gen
|
|
38
|
+
const colors = {
|
|
39
|
+
background: '#1a1a1a', // slate-800
|
|
40
|
+
text: '#f8fafc', // slate-50
|
|
41
|
+
textSecondary: '#888888', // slate-400
|
|
42
|
+
border: '#2a2a2a', // slate-700
|
|
43
|
+
inputBg: '#0f0f0f', // slate-900
|
|
44
|
+
};
|
|
45
|
+
|
|
18
46
|
return (
|
|
19
|
-
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.
|
|
20
|
-
<div
|
|
21
|
-
|
|
22
|
-
|
|
47
|
+
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.7)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1300 }}>
|
|
48
|
+
<div
|
|
49
|
+
ref={modalRef}
|
|
50
|
+
role="dialog"
|
|
51
|
+
aria-modal="true"
|
|
52
|
+
aria-labelledby="ai-feedback-title"
|
|
53
|
+
tabIndex={-1}
|
|
54
|
+
style={{ background: colors.background, padding: 16, width: 480, borderRadius: 8, border: `1px solid ${colors.border}`, color: colors.text, outline: 'none' }}
|
|
55
|
+
>
|
|
56
|
+
<h3 id="ai-feedback-title" style={{ color: colors.text, margin: 0 }}>AI Suggestion</h3>
|
|
57
|
+
<p style={{ marginTop: 8, color: colors.text }}>
|
|
23
58
|
{suggestion?.text ?? "No suggestion"}
|
|
24
59
|
{suggestion?.confidence != null ? ` (confidence ${Math.round((suggestion.confidence || 0) * 100)}%)` : null}
|
|
25
60
|
</p>
|
|
26
|
-
{suggestion?.reasoning ? <small>{suggestion.reasoning}</small> : null}
|
|
61
|
+
{suggestion?.reasoning ? <small style={{ color: colors.textSecondary }}>{suggestion.reasoning}</small> : null}
|
|
27
62
|
|
|
28
|
-
<div style={{ marginTop: 12 }}>
|
|
29
|
-
<label>Action</label>
|
|
30
|
-
<select value={reviewerAction} onChange={(e) => setAction(e.target.value)} style={{
|
|
63
|
+
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
64
|
+
<label htmlFor="ai-action-select" style={{ color: colors.text }}>Action</label>
|
|
65
|
+
<select id="ai-action-select" value={reviewerAction} onChange={(e) => setAction(e.target.value)} style={{ background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px' }}>
|
|
31
66
|
<option value="ACCEPT">Accept</option>
|
|
32
67
|
<option value="REJECT">Reject</option>
|
|
33
68
|
<option value="MODIFY">Modify</option>
|
|
@@ -35,15 +70,15 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
|
|
|
35
70
|
</div>
|
|
36
71
|
|
|
37
72
|
{reviewerAction === "MODIFY" ? (
|
|
38
|
-
<div style={{ marginTop: 12 }}>
|
|
39
|
-
<label>Final Text</label>
|
|
40
|
-
<input value={finalText} onChange={(e) => setFinalText(e.target.value)} style={{
|
|
73
|
+
<div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
74
|
+
<label htmlFor="ai-final-text" style={{ color: colors.text }}>Final Text</label>
|
|
75
|
+
<input id="ai-final-text" value={finalText} onChange={(e) => setFinalText(e.target.value)} style={{ width: "100%", background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px', boxSizing: 'border-box' }} />
|
|
41
76
|
</div>
|
|
42
77
|
) : null}
|
|
43
78
|
|
|
44
|
-
<div style={{ marginTop: 12 }}>
|
|
45
|
-
<label>Reason</label>
|
|
46
|
-
<select value={reasonCategory} onChange={(e) => setReason(e.target.value)} style={{
|
|
79
|
+
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
80
|
+
<label htmlFor="ai-reason-select" style={{ color: colors.text }}>Reason</label>
|
|
81
|
+
<select id="ai-reason-select" value={reasonCategory} onChange={(e) => setReason(e.target.value)} style={{ background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px' }}>
|
|
47
82
|
<option value="AI_CORRECT">AI_CORRECT</option>
|
|
48
83
|
<option value="AI_INCORRECT">AI_INCORRECT</option>
|
|
49
84
|
<option value="AI_SUBOPTIMAL">AI_SUBOPTIMAL</option>
|
|
@@ -52,17 +87,18 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
|
|
|
52
87
|
</select>
|
|
53
88
|
</div>
|
|
54
89
|
|
|
55
|
-
<div style={{ marginTop: 12 }}>
|
|
56
|
-
<label>Details</label>
|
|
57
|
-
<textarea value={reasonDetail} onChange={(e) => setDetail(e.target.value)} style={{
|
|
90
|
+
<div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
91
|
+
<label htmlFor="ai-details-textarea" style={{ color: colors.text }}>Details</label>
|
|
92
|
+
<textarea id="ai-details-textarea" value={reasonDetail} onChange={(e) => setDetail(e.target.value)} style={{ width: "100%", background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px', boxSizing: 'border-box' }} />
|
|
58
93
|
</div>
|
|
59
94
|
|
|
60
95
|
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 }}>
|
|
61
|
-
<button onClick={onClose}>Cancel</button>
|
|
96
|
+
<button onClick={onClose} style={{ background: colors.border, color: colors.text, border: 'none', borderRadius: 4, padding: '6px 12px', cursor: 'pointer' }}>Cancel</button>
|
|
62
97
|
<button
|
|
63
98
|
onClick={() =>
|
|
64
99
|
onSubmit({ reviewerAction, finalText: finalText || undefined, reasonCategory, reasonDetail: reasonDetail || undefined })
|
|
65
100
|
}
|
|
101
|
+
style={{ background: '#f97316', color: '#fff', border: 'none', borderRadius: 4, padding: '6px 12px', cursor: 'pointer' }}
|
|
66
102
|
>
|
|
67
103
|
Submit
|
|
68
104
|
</button>
|
|
@@ -73,5 +109,3 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
|
|
|
73
109
|
};
|
|
74
110
|
|
|
75
111
|
export default AIFeedbackModal;
|
|
76
|
-
|
|
77
|
-
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Box, IconButton, Tooltip, Typography, useTheme } from '@mui/material'
|
|
2
|
+
import { Sun, Moon } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface AppHeaderProps {
|
|
5
|
+
isDarkMode?: boolean
|
|
6
|
+
onToggleTheme?: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function AppHeader({ isDarkMode = true, onToggleTheme }: AppHeaderProps) {
|
|
10
|
+
const theme = useTheme()
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Box
|
|
14
|
+
component="header"
|
|
15
|
+
sx={{
|
|
16
|
+
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
17
|
+
backgroundColor: theme.palette.background.paper,
|
|
18
|
+
backdropFilter: 'blur(8px)',
|
|
19
|
+
position: 'sticky',
|
|
20
|
+
top: 0,
|
|
21
|
+
zIndex: 1100,
|
|
22
|
+
px: 2,
|
|
23
|
+
py: 1.5,
|
|
24
|
+
display: 'flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
justifyContent: 'space-between',
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|
30
|
+
<img src="/nomad-karaoke-logo.svg" alt="Nomad Karaoke" style={{ height: 40 }} />
|
|
31
|
+
<Typography
|
|
32
|
+
variant="h6"
|
|
33
|
+
sx={{
|
|
34
|
+
fontWeight: 'bold',
|
|
35
|
+
color: theme.palette.text.primary,
|
|
36
|
+
fontSize: '1.1rem',
|
|
37
|
+
lineHeight: 1,
|
|
38
|
+
m: 0,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
Lyrics Transcription Review
|
|
42
|
+
</Typography>
|
|
43
|
+
</Box>
|
|
44
|
+
|
|
45
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
46
|
+
{onToggleTheme && (
|
|
47
|
+
<Tooltip title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}>
|
|
48
|
+
<IconButton
|
|
49
|
+
onClick={onToggleTheme}
|
|
50
|
+
sx={{
|
|
51
|
+
color: theme.palette.text.secondary,
|
|
52
|
+
'&:hover': {
|
|
53
|
+
color: theme.palette.text.primary,
|
|
54
|
+
backgroundColor: theme.palette.action.hover,
|
|
55
|
+
},
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{isDarkMode ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
|
59
|
+
</IconButton>
|
|
60
|
+
</Tooltip>
|
|
61
|
+
)}
|
|
62
|
+
</Box>
|
|
63
|
+
</Box>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -42,7 +42,7 @@ const WordContainer = styled(Box, {
|
|
|
42
42
|
'50%': { opacity: 0.5 }
|
|
43
43
|
},
|
|
44
44
|
'&:hover': {
|
|
45
|
-
backgroundColor: '
|
|
45
|
+
backgroundColor: 'rgba(34, 197, 94, 0.35)' // green tint hover for dark mode
|
|
46
46
|
}
|
|
47
47
|
}))
|
|
48
48
|
|
|
@@ -51,7 +51,7 @@ const OriginalWordLabel = styled(Box)({
|
|
|
51
51
|
top: '-14px',
|
|
52
52
|
left: '0',
|
|
53
53
|
fontSize: '0.6rem',
|
|
54
|
-
color: '#
|
|
54
|
+
color: '#888888', // slate-400 for dark mode
|
|
55
55
|
textDecoration: 'line-through',
|
|
56
56
|
opacity: 0.7,
|
|
57
57
|
whiteSpace: 'nowrap',
|
|
@@ -71,10 +71,10 @@ const ActionButton = styled(IconButton)(({ theme }) => ({
|
|
|
71
71
|
minHeight: '20px',
|
|
72
72
|
width: '20px',
|
|
73
73
|
height: '20px',
|
|
74
|
-
backgroundColor: 'rgba(
|
|
75
|
-
border: '1px solid rgba(
|
|
74
|
+
backgroundColor: 'rgba(30, 41, 59, 0.9)', // slate-800 with opacity for dark mode
|
|
75
|
+
border: '1px solid rgba(248, 250, 252, 0.1)', // light border for dark mode
|
|
76
76
|
'&:hover': {
|
|
77
|
-
backgroundColor: 'rgba(
|
|
77
|
+
backgroundColor: 'rgba(51, 65, 85, 1)', // slate-700 for dark mode
|
|
78
78
|
transform: 'scale(1.1)'
|
|
79
79
|
},
|
|
80
80
|
'& .MuiSvgIcon-root': {
|