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.
Files changed (47) hide show
  1. karaoke_gen/audio_fetcher.py +984 -33
  2. karaoke_gen/audio_processor.py +4 -0
  3. karaoke_gen/instrumental_review/static/index.html +37 -14
  4. karaoke_gen/karaoke_finalise/karaoke_finalise.py +25 -1
  5. karaoke_gen/karaoke_gen.py +208 -39
  6. karaoke_gen/lyrics_processor.py +111 -31
  7. karaoke_gen/utils/__init__.py +26 -0
  8. karaoke_gen/utils/cli_args.py +15 -6
  9. karaoke_gen/utils/gen_cli.py +30 -5
  10. karaoke_gen/utils/remote_cli.py +301 -20
  11. {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/METADATA +107 -5
  12. {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/RECORD +47 -43
  13. lyrics_transcriber/core/controller.py +76 -2
  14. lyrics_transcriber/frontend/index.html +5 -1
  15. lyrics_transcriber/frontend/package-lock.json +4553 -0
  16. lyrics_transcriber/frontend/package.json +4 -1
  17. lyrics_transcriber/frontend/playwright.config.ts +69 -0
  18. lyrics_transcriber/frontend/public/nomad-karaoke-logo.svg +5 -0
  19. lyrics_transcriber/frontend/src/App.tsx +94 -63
  20. lyrics_transcriber/frontend/src/api.ts +25 -10
  21. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +55 -21
  22. lyrics_transcriber/frontend/src/components/AppHeader.tsx +65 -0
  23. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +5 -5
  24. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +9 -9
  25. lyrics_transcriber/frontend/src/components/EditModal.tsx +1 -1
  26. lyrics_transcriber/frontend/src/components/EditWordList.tsx +1 -1
  27. lyrics_transcriber/frontend/src/components/Header.tsx +34 -48
  28. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +22 -21
  29. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  30. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  31. lyrics_transcriber/frontend/src/components/WordDivider.tsx +3 -3
  32. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +2 -2
  33. lyrics_transcriber/frontend/src/components/shared/constants.ts +15 -5
  34. lyrics_transcriber/frontend/src/main.tsx +1 -7
  35. lyrics_transcriber/frontend/src/theme.ts +337 -135
  36. lyrics_transcriber/frontend/vite.config.ts +5 -0
  37. lyrics_transcriber/frontend/web_assets/assets/{index-COYImAcx.js → index-BECn1o8Q.js} +38 -22
  38. lyrics_transcriber/frontend/web_assets/assets/{index-COYImAcx.js.map → index-BECn1o8Q.js.map} +1 -1
  39. lyrics_transcriber/frontend/web_assets/index.html +1 -1
  40. lyrics_transcriber/frontend/yarn.lock +1005 -1046
  41. lyrics_transcriber/output/countdown_processor.py +39 -0
  42. lyrics_transcriber/review/server.py +1 -1
  43. lyrics_transcriber/transcribers/audioshake.py +96 -7
  44. lyrics_transcriber/types.py +14 -12
  45. {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/WHEEL +0 -0
  46. {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/entry_points.txt +0 -0
  47. {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.82.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 { useEffect, useState } from 'react'
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
- setApiClient(new LiveApiClient(baseApiUrl))
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
- <Box sx={{ p: 3 }}>
147
- {error && (
148
- <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
149
- {error}
150
- </Alert>
151
- )}
152
- {isReadOnly ? (
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
- <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
158
- <Typography variant="h4">
159
- Lyrics Correction Review
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
- <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
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
- <Box sx={{
186
- p: 1.5,
187
- pb: 3,
188
- maxWidth: '100%',
189
- overflowX: 'hidden'
190
- }}>
191
- {error && (
192
- <Alert severity="error" sx={{ mb: 1 }} onClose={() => setError(null)}>
193
- {error}
194
- </Alert>
195
- )}
196
- {isReadOnly && (
197
- <Alert severity="info" sx={{ mb: 1 }}>
198
- Running in read-only mode. Connect to an API to enable editing.
199
- </Alert>
200
- )}
201
- <LyricsAnalyzer
202
- data={data}
203
- onFileLoad={handleFileLoad}
204
- onShowMetadata={() => setShowMetadata(true)}
205
- apiClient={apiClient}
206
- isReadOnly={isReadOnly}
207
- audioHash={audioHash}
208
- />
209
- {renderMetadataModal()}
210
- </Box>
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
- constructor(private baseUrl: string) {
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(`${this.baseUrl}/correction-data`);
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(`${this.baseUrl}/complete`, {
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 `${this.baseUrl}/audio/${audioHash}`
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(`${this.baseUrl}/preview-video`, {
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 `${this.baseUrl}/preview-video/${previewHash}`;
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(`${this.baseUrl}/handlers`, {
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(`${this.baseUrl}/add-lyrics`, {
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(`${this.baseUrl}/v1/annotations`, {
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(`${this.baseUrl}/v1/annotations/stats`);
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.5)", display: "flex", alignItems: "center", justifyContent: "center" }}>
20
- <div style={{ background: "#fff", padding: 16, width: 480, borderRadius: 8 }}>
21
- <h3>AI Suggestion</h3>
22
- <p style={{ marginTop: 8 }}>
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={{ marginLeft: 8 }}>
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={{ marginLeft: 8, width: "100%" }} />
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={{ marginLeft: 8 }}>
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={{ marginLeft: 8, width: "100%" }} />
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: '#c8e6c9'
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: '#666',
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(255, 255, 255, 0.9)',
75
- border: '1px solid rgba(0, 0, 0, 0.1)',
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(255, 255, 255, 1)',
77
+ backgroundColor: 'rgba(51, 65, 85, 1)', // slate-700 for dark mode
78
78
  transform: 'scale(1.1)'
79
79
  },
80
80
  '& .MuiSvgIcon-root': {