Spaces:
Running
Running
Commit
·
ad438b8
1
Parent(s):
8c7d858
update: export from starry-refactor 2026-02-21 (add example score seed)
Browse files- .gitattributes +1 -0
- Dockerfile +5 -0
- backend/omr-service/src/db/client.ts +2 -2
- backend/omr-service/src/lib/dbSolutionStore.ts +5 -1
- backend/omr-service/src/routes/issueMeasures.ts +6 -0
- backend/omr-service/src/services/issueMeasure.service.ts +5 -0
- backend/omr-service/src/services/musicSet.service.ts +6 -4
- docker-entrypoint.sh +5 -0
- example-score/page-00.png +3 -0
- example-score/page-01.png +3 -0
- example-score/page-02.png +3 -0
- example-score/page-03.png +3 -0
- seed-example.sh +146 -0
.gitattributes
CHANGED
|
@@ -38,3 +38,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 38 |
*.eot filter=lfs diff=lfs merge=lfs -text
|
| 39 |
*.otf filter=lfs diff=lfs merge=lfs -text
|
| 40 |
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 38 |
*.eot filter=lfs diff=lfs merge=lfs -text
|
| 39 |
*.otf filter=lfs diff=lfs merge=lfs -text
|
| 40 |
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
CHANGED
|
@@ -123,6 +123,11 @@ COPY --chown=node backend/python-services/ ./backend/python-services/
|
|
| 123 |
# --- Supervisord config ---
|
| 124 |
COPY --chown=node supervisord.conf ./supervisord.conf
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
# --- Config files ---
|
| 127 |
COPY --chown=node docker-entrypoint.sh ./docker-entrypoint.sh
|
| 128 |
COPY --chown=node nginx.conf /etc/nginx/nginx.conf
|
|
|
|
| 123 |
# --- Supervisord config ---
|
| 124 |
COPY --chown=node supervisord.conf ./supervisord.conf
|
| 125 |
|
| 126 |
+
# --- Example score for seeding ---
|
| 127 |
+
COPY --chown=node example-score/ ./example-score/
|
| 128 |
+
COPY --chown=node seed-example.sh ./seed-example.sh
|
| 129 |
+
RUN chmod +x seed-example.sh
|
| 130 |
+
|
| 131 |
# --- Config files ---
|
| 132 |
COPY --chown=node docker-entrypoint.sh ./docker-entrypoint.sh
|
| 133 |
COPY --chown=node nginx.conf /etc/nginx/nginx.conf
|
backend/omr-service/src/db/client.ts
CHANGED
|
@@ -9,9 +9,9 @@ export const pool = new Pool({
|
|
| 9 |
database: config.database.database,
|
| 10 |
user: config.database.user,
|
| 11 |
password: config.database.password,
|
| 12 |
-
max:
|
| 13 |
idleTimeoutMillis: 30000,
|
| 14 |
-
connectionTimeoutMillis:
|
| 15 |
});
|
| 16 |
|
| 17 |
export async function query<T extends pg.QueryResultRow = any>(text: string, params?: any[]): Promise<pg.QueryResult<T>> {
|
|
|
|
| 9 |
database: config.database.database,
|
| 10 |
user: config.database.user,
|
| 11 |
password: config.database.password,
|
| 12 |
+
max: 20,
|
| 13 |
idleTimeoutMillis: 30000,
|
| 14 |
+
connectionTimeoutMillis: 10000,
|
| 15 |
});
|
| 16 |
|
| 17 |
export async function query<T extends pg.QueryResultRow = any>(text: string, params?: any[]): Promise<pg.QueryResult<T>> {
|
backend/omr-service/src/lib/dbSolutionStore.ts
CHANGED
|
@@ -7,7 +7,11 @@ export const DbSolutionStore = {
|
|
| 7 |
return result ?? null;
|
| 8 |
},
|
| 9 |
async set(key: string, val: any) {
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
},
|
| 12 |
async batchGet(keys: string[]) {
|
| 13 |
return solutionCacheService.batchGet(keys);
|
|
|
|
| 7 |
return result ?? null;
|
| 8 |
},
|
| 9 |
async set(key: string, val: any) {
|
| 10 |
+
try {
|
| 11 |
+
await solutionCacheService.set(key, val);
|
| 12 |
+
} catch (err) {
|
| 13 |
+
console.error('[DbSolutionStore] set failed (non-fatal):', (err as Error).message);
|
| 14 |
+
}
|
| 15 |
},
|
| 16 |
async batchGet(keys: string[]) {
|
| 17 |
return solutionCacheService.batchGet(keys);
|
backend/omr-service/src/routes/issueMeasures.ts
CHANGED
|
@@ -51,4 +51,10 @@ export default async function issueMeasuresRoutes(fastify: FastifyInstance) {
|
|
| 51 |
|
| 52 |
return { code: 0, data: result };
|
| 53 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
|
|
|
| 51 |
|
| 52 |
return { code: 0, data: result };
|
| 53 |
});
|
| 54 |
+
|
| 55 |
+
// Delete all issue measures for a score
|
| 56 |
+
fastify.delete<{ Params: ScoreParams }>('/scores/:id/issueMeasures', async (request) => {
|
| 57 |
+
const count = await issueMeasureService.deleteByScore(request.params.id);
|
| 58 |
+
return { code: 0, data: { deleted: count } };
|
| 59 |
+
});
|
| 60 |
}
|
backend/omr-service/src/services/issueMeasure.service.ts
CHANGED
|
@@ -97,3 +97,8 @@ export async function upsert(scoreId: string, measureIndex: number, measure: any
|
|
| 97 |
|
| 98 |
return rowToResult(row);
|
| 99 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
return rowToResult(row);
|
| 99 |
}
|
| 100 |
+
|
| 101 |
+
export async function deleteByScore(scoreId: string): Promise<number> {
|
| 102 |
+
const result = await query('DELETE FROM issue_measures WHERE score_id = $1', [scoreId]);
|
| 103 |
+
return result.rowCount ?? 0;
|
| 104 |
+
}
|
backend/omr-service/src/services/musicSet.service.ts
CHANGED
|
@@ -65,7 +65,7 @@ export async function createMusicSet(input: CreateMusicSetInput): Promise<MusicS
|
|
| 65 |
|
| 66 |
export async function getMusicSet(id: string): Promise<MusicSet | null> {
|
| 67 |
const { rows } = await query<MusicSet>(
|
| 68 |
-
`SELECT ms.*,
|
| 69 |
(SELECT COALESCE(json_agg(json_build_object('id', t.id, 'name', t.name)), '[]'::json)
|
| 70 |
FROM music_set_tags mst
|
| 71 |
JOIN tags t ON mst.tag_id = t.id
|
|
@@ -98,8 +98,10 @@ export async function listMusicSets(params: ListParams): Promise<{ rows: MusicSe
|
|
| 98 |
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
| 99 |
|
| 100 |
// Validate sortedBy to prevent SQL injection
|
|
|
|
| 101 |
const allowedSortFields = ['name', 'type', 'updated_at', 'created_at'];
|
| 102 |
-
const
|
|
|
|
| 103 |
const sortOrder = sortedType === 'asc' ? 'ASC' : 'DESC';
|
| 104 |
|
| 105 |
// Get count
|
|
@@ -109,7 +111,7 @@ export async function listMusicSets(params: ListParams): Promise<{ rows: MusicSe
|
|
| 109 |
// Get rows with tags
|
| 110 |
values.push(limit, offset);
|
| 111 |
const { rows } = await query<MusicSet>(
|
| 112 |
-
`SELECT ms.*,
|
| 113 |
(SELECT COALESCE(json_agg(json_build_object('id', t.id, 'name', t.name)), '[]'::json)
|
| 114 |
FROM music_set_tags mst
|
| 115 |
JOIN tags t ON mst.tag_id = t.id
|
|
@@ -160,7 +162,7 @@ export async function updateMusicSet(id: string, input: UpdateMusicSetInput): Pr
|
|
| 160 |
|
| 161 |
// Return updated music set with tags
|
| 162 |
const { rows } = await client.query<MusicSet>(
|
| 163 |
-
`SELECT ms.*,
|
| 164 |
(SELECT COALESCE(json_agg(json_build_object('id', t.id, 'name', t.name)), '[]'::json)
|
| 165 |
FROM music_set_tags mst
|
| 166 |
JOIN tags t ON mst.tag_id = t.id
|
|
|
|
| 65 |
|
| 66 |
export async function getMusicSet(id: string): Promise<MusicSet | null> {
|
| 67 |
const { rows } = await query<MusicSet>(
|
| 68 |
+
`SELECT ms.*, ms.updated_at AS "lastUpdateAt",
|
| 69 |
(SELECT COALESCE(json_agg(json_build_object('id', t.id, 'name', t.name)), '[]'::json)
|
| 70 |
FROM music_set_tags mst
|
| 71 |
JOIN tags t ON mst.tag_id = t.id
|
|
|
|
| 98 |
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
| 99 |
|
| 100 |
// Validate sortedBy to prevent SQL injection
|
| 101 |
+
const sortFieldMap: Record<string, string> = { lastUpdateAt: 'updated_at' };
|
| 102 |
const allowedSortFields = ['name', 'type', 'updated_at', 'created_at'];
|
| 103 |
+
const resolved = sortFieldMap[sortedBy] || sortedBy;
|
| 104 |
+
const sortField = allowedSortFields.includes(resolved) ? resolved : 'updated_at';
|
| 105 |
const sortOrder = sortedType === 'asc' ? 'ASC' : 'DESC';
|
| 106 |
|
| 107 |
// Get count
|
|
|
|
| 111 |
// Get rows with tags
|
| 112 |
values.push(limit, offset);
|
| 113 |
const { rows } = await query<MusicSet>(
|
| 114 |
+
`SELECT ms.*, ms.updated_at AS "lastUpdateAt",
|
| 115 |
(SELECT COALESCE(json_agg(json_build_object('id', t.id, 'name', t.name)), '[]'::json)
|
| 116 |
FROM music_set_tags mst
|
| 117 |
JOIN tags t ON mst.tag_id = t.id
|
|
|
|
| 162 |
|
| 163 |
// Return updated music set with tags
|
| 164 |
const { rows } = await client.query<MusicSet>(
|
| 165 |
+
`SELECT ms.*, ms.updated_at AS "lastUpdateAt",
|
| 166 |
(SELECT COALESCE(json_agg(json_build_object('id', t.id, 'name', t.name)), '[]'::json)
|
| 167 |
FROM music_set_tags mst
|
| 168 |
JOIN tags t ON mst.tag_id = t.id
|
docker-entrypoint.sh
CHANGED
|
@@ -256,6 +256,11 @@ for i in $(seq 1 30); do
|
|
| 256 |
sleep 1
|
| 257 |
done
|
| 258 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
# ── Start nginx (port 7860, foreground) ──
|
| 260 |
echo 'Starting nginx on port 7860...'
|
| 261 |
exec nginx -g 'daemon off;'
|
|
|
|
| 256 |
sleep 1
|
| 257 |
done
|
| 258 |
|
| 259 |
+
# ── Seed example score (background — waits for ML predictors internally) ──
|
| 260 |
+
if [ -f /home/node/app/seed-example.sh ]; then
|
| 261 |
+
/home/node/app/seed-example.sh &
|
| 262 |
+
fi
|
| 263 |
+
|
| 264 |
# ── Start nginx (port 7860, foreground) ──
|
| 265 |
echo 'Starting nginx on port 7860...'
|
| 266 |
exec nginx -g 'daemon off;'
|
example-score/page-00.png
ADDED
|
Git LFS Details
|
example-score/page-01.png
ADDED
|
Git LFS Details
|
example-score/page-02.png
ADDED
|
Git LFS Details
|
example-score/page-03.png
ADDED
|
Git LFS Details
|
seed-example.sh
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#
|
| 3 |
+
# Seed the HF Space with a pre-recognized example score.
|
| 4 |
+
#
|
| 5 |
+
# Runs in background after services start. Waits for ML predictors,
|
| 6 |
+
# then uploads example page images through predict/pages pipeline.
|
| 7 |
+
#
|
| 8 |
+
set -euo pipefail
|
| 9 |
+
|
| 10 |
+
API_BASE="http://127.0.0.1:3080"
|
| 11 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 12 |
+
IMAGE_DIR="$SCRIPT_DIR/example-score"
|
| 13 |
+
SCORE_ID="c8d853b1-f18b-5692-a113-39692e17f395"
|
| 14 |
+
SCORE_TITLE="One Summer's Day"
|
| 15 |
+
MAX_WAIT=900 # 15 min for model download + load on CPU
|
| 16 |
+
POLL_INTERVAL=5
|
| 17 |
+
|
| 18 |
+
log() { echo "[seed] $*"; }
|
| 19 |
+
|
| 20 |
+
# ── Step 0: Check if score already exists with data ──────────────────────
|
| 21 |
+
log "Checking if example score already exists..."
|
| 22 |
+
EXISTING=$(curl -sf "$API_BASE/api/scores/$SCORE_ID" 2>/dev/null || echo "")
|
| 23 |
+
if echo "$EXISTING" | python3 -c "
|
| 24 |
+
import json, sys
|
| 25 |
+
try:
|
| 26 |
+
d = json.load(sys.stdin)
|
| 27 |
+
pages = d.get('data', d).get('pages', [])
|
| 28 |
+
if len(pages) > 0:
|
| 29 |
+
sys.exit(0)
|
| 30 |
+
except: pass
|
| 31 |
+
sys.exit(1)
|
| 32 |
+
" 2>/dev/null; then
|
| 33 |
+
log "Example score already has pages, skipping seed."
|
| 34 |
+
exit 0
|
| 35 |
+
fi
|
| 36 |
+
|
| 37 |
+
# ── Step 1: Wait for ML predictors ──────────────────────────────────────
|
| 38 |
+
log "Waiting for ML predictors to be ready (up to ${MAX_WAIT}s)..."
|
| 39 |
+
ELAPSED=0
|
| 40 |
+
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
| 41 |
+
# Try a lightweight predict/layout call to check if the layout predictor is up.
|
| 42 |
+
# We send a tiny 1x1 PNG as a probe — if it responds (even with an error result),
|
| 43 |
+
# the predictor is loaded.
|
| 44 |
+
PROBE_RES=$(curl -sf -X POST "$API_BASE/api/predict/layout" \
|
| 45 |
+
-H "Content-Type: application/json" \
|
| 46 |
+
-d '{"images":["iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="]}' \
|
| 47 |
+
2>/dev/null || echo "")
|
| 48 |
+
if [ -n "$PROBE_RES" ]; then
|
| 49 |
+
log "Predictors are ready."
|
| 50 |
+
break
|
| 51 |
+
fi
|
| 52 |
+
sleep 10
|
| 53 |
+
ELAPSED=$((ELAPSED + 10))
|
| 54 |
+
if [ $((ELAPSED % 60)) -eq 0 ]; then
|
| 55 |
+
log " Still waiting... (${ELAPSED}s elapsed)"
|
| 56 |
+
fi
|
| 57 |
+
done
|
| 58 |
+
|
| 59 |
+
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
| 60 |
+
log "WARNING: Timed out waiting for predictors. Attempting seed anyway..."
|
| 61 |
+
fi
|
| 62 |
+
|
| 63 |
+
# ── Step 2: Create score shell ───────────────────────────────────────────
|
| 64 |
+
log "Creating score shell ($SCORE_ID)..."
|
| 65 |
+
curl -sf -X PUT "$API_BASE/api/scores/$SCORE_ID/data" \
|
| 66 |
+
-H "Content-Type: application/json" \
|
| 67 |
+
-d '{"pages":[],"patches":[],"tags":[]}' > /dev/null
|
| 68 |
+
|
| 69 |
+
# ── Step 3: Upload images + predict ──────────────────────────────────────
|
| 70 |
+
log "Submitting predict/pages for 4 pages..."
|
| 71 |
+
CURL_ARGS=(-F 'processes=["gauge","semantic","mask","brackets","text"]')
|
| 72 |
+
for i in 0 1 2 3; do
|
| 73 |
+
IMG="$IMAGE_DIR/page-0${i}.png"
|
| 74 |
+
if [ ! -f "$IMG" ]; then
|
| 75 |
+
log "ERROR: Missing image: $IMG"
|
| 76 |
+
exit 1
|
| 77 |
+
fi
|
| 78 |
+
CURL_ARGS+=(-F "page${i}=@${IMG};type=image/png")
|
| 79 |
+
done
|
| 80 |
+
|
| 81 |
+
PREDICT_RES=$(curl -sf -X POST "$API_BASE/api/predict/pages/$SCORE_ID" "${CURL_ARGS[@]}")
|
| 82 |
+
TASK_ID=$(echo "$PREDICT_RES" | python3 -c "import json,sys; print(json.load(sys.stdin)['task_id'])")
|
| 83 |
+
log "Task ID: $TASK_ID"
|
| 84 |
+
|
| 85 |
+
# ── Step 4: Poll task ────────────────────────────────────────────────────
|
| 86 |
+
log "Polling task..."
|
| 87 |
+
ELAPSED=0
|
| 88 |
+
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
| 89 |
+
POLL_RES=$(curl -sf "$API_BASE/api/tasks/$TASK_ID/poll" 2>/dev/null || echo '{"status":"unknown"}')
|
| 90 |
+
STATUS=$(echo "$POLL_RES" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status','unknown'))" 2>/dev/null || echo "unknown")
|
| 91 |
+
|
| 92 |
+
case "$STATUS" in
|
| 93 |
+
completed)
|
| 94 |
+
log "Prediction completed!"
|
| 95 |
+
echo "$POLL_RES" | python3 -c "
|
| 96 |
+
import json, sys
|
| 97 |
+
r = json.load(sys.stdin).get('result', {})
|
| 98 |
+
print(f'[seed] Systems: {r.get(\"systems\",\"?\")}, Staves: {r.get(\"staves\",\"?\")}, StaffLayout: {r.get(\"staffLayout\",\"?\")}')
|
| 99 |
+
" 2>/dev/null || true
|
| 100 |
+
break
|
| 101 |
+
;;
|
| 102 |
+
failed|error)
|
| 103 |
+
log "ERROR: Prediction task failed!"
|
| 104 |
+
echo "$POLL_RES" | python3 -m json.tool 2>/dev/null || echo "$POLL_RES"
|
| 105 |
+
exit 1
|
| 106 |
+
;;
|
| 107 |
+
*)
|
| 108 |
+
sleep $POLL_INTERVAL
|
| 109 |
+
ELAPSED=$((ELAPSED + POLL_INTERVAL))
|
| 110 |
+
;;
|
| 111 |
+
esac
|
| 112 |
+
done
|
| 113 |
+
|
| 114 |
+
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
| 115 |
+
log "ERROR: Task polling timed out after ${MAX_WAIT}s"
|
| 116 |
+
exit 1
|
| 117 |
+
fi
|
| 118 |
+
|
| 119 |
+
# ── Step 5: Set title ────────────────────────────────────────────────────
|
| 120 |
+
log "Setting score title: $SCORE_TITLE"
|
| 121 |
+
curl -sf -X PUT "$API_BASE/api/scores/$SCORE_ID" \
|
| 122 |
+
-H "Content-Type: application/json" \
|
| 123 |
+
-d "{\"title\":\"$SCORE_TITLE\"}" > /dev/null || log "WARNING: Failed to set title"
|
| 124 |
+
|
| 125 |
+
# ── Step 6: Regulation (optional) ────────────────────────────────────────
|
| 126 |
+
log "Running regulation..."
|
| 127 |
+
REG_RES=$(curl -sf -X POST "$API_BASE/api/scores/$SCORE_ID/regulate" --max-time 600 2>/dev/null || echo "")
|
| 128 |
+
if [ -n "$REG_RES" ]; then
|
| 129 |
+
echo "$REG_RES" | python3 -c "
|
| 130 |
+
import json, sys
|
| 131 |
+
r = json.load(sys.stdin)
|
| 132 |
+
if r.get('code') == 0:
|
| 133 |
+
s = r['data']['stat']
|
| 134 |
+
qs = s.get('qualityScore', 0)
|
| 135 |
+
m = s.get('measures', {})
|
| 136 |
+
print(f'[seed] Quality: {qs*100:.1f}%')
|
| 137 |
+
print(f'[seed] Measures: solved={m.get(\"solved\",0)} issue={m.get(\"issue\",0)} fatal={m.get(\"fatal\",0)}')
|
| 138 |
+
else:
|
| 139 |
+
print(f'[seed] Regulation error: {r.get(\"message\",\"unknown\")}')
|
| 140 |
+
" 2>/dev/null || true
|
| 141 |
+
else
|
| 142 |
+
log "WARNING: Regulation failed or timed out (non-critical)."
|
| 143 |
+
fi
|
| 144 |
+
|
| 145 |
+
log "=== Seed complete ==="
|
| 146 |
+
log "View at: /playground/$SCORE_ID"
|