travail d'un we (j'en ai big marre)
1
.cache
@@ -1 +0,0 @@
|
||||
{"access_token": "BQDeVV_yylewo72n9ZtSMTn1WNhJKSbUBDKv18Cl-QY7dphidwC9jDuGuZWKaIWZv-fVIG1uuP1XvJFKwcj3GOxgRxYJbaWOLtogCHkAxC-iOaZe6uphn-vCy5rbX6Xlx9XVUG-jHNu0jaCf1-ueTRUQuyF0Btu4_aAcIktTnYyofSUJuk_Mdi1J6V3hmARkCl34Rv-4hgfXcIqQ_rpdCFHGheEU1rSL5oME", "token_type": "Bearer", "expires_in": 3600, "scope": "user-modify-playback-state user-read-playback-state", "expires_at": 1757966041, "refresh_token": "AQAxD3wm_8aDlK9w2ZsibKsrPY0SxUJb2UaqiT2ygxK-pEq2ojxPzgLa6DNt9EpP3mjFDCLmGO5glj5fSpR3ZnqEa26belG4EQv5FOeee38ix31o2xuxPbDsY44KR5Uj1KE"}
|
||||
10
final-loop/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt ./
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY src/ .
|
||||
|
||||
1
final-loop/client-cache/credentials.json
Normal file
@@ -0,0 +1 @@
|
||||
{"username":"mathias-120","auth_type":1,"auth_data":"QWdDZF9iOVZDTUltQ0Noc0lnSFdfNU5QOXBMdFdiQllFclJ3dHM3ZVRaY1lzUEpKd1Jwc1pOc3FzdjFwYnRYZ2ZRSXE2Q1ZiWTBjdzRDTUYzdkJMekdqOVliRTh1UldlZlJOdF94eEgydzJUWmhtT0RlX295ODdrTzZDYXFuMW5SVEQ4TV95S2R0aEJLTDZldkxPeDlva0wtcHJNY0p6Q3VzQl9YOGFhVHNYaw=="}
|
||||
1
final-loop/client-cache/volume
Normal file
@@ -0,0 +1 @@
|
||||
65535
|
||||
34
final-loop/compose.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
librespot:
|
||||
container_name: spotvinyl-client
|
||||
image: giof71/librespot:latest
|
||||
network_mode: host
|
||||
devices:
|
||||
- /dev/snd:/dev/snd
|
||||
environment:
|
||||
- DEVICE=hw:x20,0
|
||||
- BACKEND=alsa
|
||||
- BITRATE=320
|
||||
- INITIAL_VOLUME=100
|
||||
- DEVICE_NAME=spotvinyl
|
||||
- ENABLE_OAUTH=headless # Enables OAuth headless mode
|
||||
- ENABLE_SYSTEM_CACHE=Y # Cache token for persistent login
|
||||
volumes:
|
||||
- ./client-cache:/data/system-cache
|
||||
stdin_open: true
|
||||
tty: true
|
||||
|
||||
spotvinyl:
|
||||
build: .
|
||||
container_name: spotvinyl
|
||||
devices:
|
||||
- /dev/video2
|
||||
volumes:
|
||||
- ./src:/app
|
||||
# - ./spotvinyl-cache
|
||||
working_dir: /app
|
||||
env_file:
|
||||
- .env
|
||||
command: python3 decode_barcode.py
|
||||
ports:
|
||||
- "5000:5000"
|
||||
32
final-loop/requirements.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
certifi==2025.11.12
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.4
|
||||
contourpy==1.3.3
|
||||
crccheck==1.3.1
|
||||
cycler==0.12.1
|
||||
decorator==5.2.1
|
||||
fonttools==4.60.1
|
||||
idna==3.11
|
||||
ImageIO==2.37.2
|
||||
joblib==1.5.2
|
||||
kiwisolver==1.4.9
|
||||
lazy_loader==0.4
|
||||
matplotlib==3.10.7
|
||||
networkx==3.5
|
||||
numpy==2.2.6
|
||||
opencv-python==4.12.0.88
|
||||
packaging==25.0
|
||||
pillow==12.0.0
|
||||
pyparsing==3.2.5
|
||||
python-dateutil==2.9.0.post0
|
||||
PyWavelets==1.9.0
|
||||
redis==7.1.0
|
||||
requests==2.32.5
|
||||
scikit-image==0.25.2
|
||||
scikit-learn==1.7.2
|
||||
scipy==1.16.3
|
||||
six==1.17.0
|
||||
spotipy==2.25.1
|
||||
threadpoolctl==3.6.0
|
||||
tifffile==2025.10.16
|
||||
urllib3==2.5.0
|
||||
@@ -1 +0,0 @@
|
||||
{"access_token": "BQAHRFXWQMMKWNbUO3sfhk92iUGi5VJNZmB2lTjRim-g2AIe88mgDvyc1NywjCVBTeZK-b3PkXzGDANJDDOouitEt5P8z0jpVuPrpalwAg0PzEwtKBXr8PHvacajO9B3s7Wy0s7G3CFlfGfbOfmXtW_vF8G2_ooGnlFp_oM8RcOyBl7D5Okf6kr07CWIJty4mNeTf1Ifd212u1tBnbU64lckqlQ0f0W105mt", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "AQCSe9ARCDB-Q6gzna_KKAlcEuNOx2IC9BSVXS5GZxNDi4AjJdA4Kn3hp1YPfiPfogd2rz-5zLbyjUxiRj9oTd-q5EItEPZHIVt-eMf7gCGHfj6h-hLpRrnhgMZjzRAS7CY", "scope": "user-modify-playback-state user-read-playback-state", "expires_at": 1757966002}
|
||||
@@ -1,21 +0,0 @@
|
||||
certifi>=2023.5.7
|
||||
chardet>=5.1.0
|
||||
crccheck>=1.0
|
||||
cycler>=0.11.0
|
||||
decorator>=5.1.1
|
||||
idna>=3.5
|
||||
imageio>=2.31.1
|
||||
kiwisolver>=1.4.4
|
||||
matplotlib>=3.7.1
|
||||
networkx>=3.1
|
||||
numpy>=1.24.2
|
||||
Pillow>=10.0.0
|
||||
pyparsing>=3.0.9
|
||||
python-dateutil>=2.8.2
|
||||
PyWavelets>=1.4.1
|
||||
requests>=2.31.0
|
||||
scikit-image>=0.25.2
|
||||
scipy>=1.11.1
|
||||
six>=1.16.0
|
||||
tifffile>=2023.7.23
|
||||
urllib3>=2.0.4
|
||||
@@ -1,57 +0,0 @@
|
||||
import argparse
|
||||
import pprint
|
||||
from get_heights import get_heights
|
||||
from encode_decode import spotify_bar_decode
|
||||
from get_uri import get_uri, get_info
|
||||
import cv2
|
||||
|
||||
|
||||
def process_frame(frame, token):
|
||||
heights = get_heights(frame)
|
||||
print(len(heights)) # assumes get_heights can take image array
|
||||
if len(heights) != 23:
|
||||
return None # skip bad frames
|
||||
else:
|
||||
print("ON TROUVE UN CODE")
|
||||
|
||||
heights = heights[1:11] + heights[12:-1]
|
||||
decoded = spotify_bar_decode(heights)
|
||||
uri = get_uri(decoded, token)
|
||||
summary, full_response = get_info(uri["target"], token)
|
||||
return summary
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--token", required=True)
|
||||
args = parser.parse_args()
|
||||
token = args.token
|
||||
|
||||
cap = cv2.VideoCapture(0) # 0 for default webcam
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
# Définir les dimensions et la marge du cadre
|
||||
height, width, _ = frame.shape
|
||||
margin = 100 # ajustez la taille du cadre ici
|
||||
frame = frame[margin:height - margin, margin:width - margin]
|
||||
|
||||
try:
|
||||
summary = process_frame(frame, token)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if summary:
|
||||
print("Summary:")
|
||||
pprint.pprint(summary)
|
||||
|
||||
cv2.imshow("Live Barcode", frame)
|
||||
|
||||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
break
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
86
final-loop/src/decode_barcode.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import argparse
|
||||
import pprint
|
||||
from get_heights import get_heights
|
||||
from encode_decode import spotify_bar_decode
|
||||
from get_uri import get_uri, get_info
|
||||
import cv2
|
||||
from start_song import start_song
|
||||
import os
|
||||
from flask import Flask, Response
|
||||
import threading
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
os.environ["QT_QPA_PLATFORM"] = "xcb"
|
||||
def process_frame(frame, token):
|
||||
|
||||
heights,preprocess = get_heights(frame)
|
||||
cv2.imshow("frame", preprocess)
|
||||
if len(heights)!=23:
|
||||
return None # skip bad frames
|
||||
else:
|
||||
print("ON TROUVE UN CODE")
|
||||
|
||||
|
||||
heights = heights[1:11] + heights[12:-1]
|
||||
decoded = spotify_bar_decode(heights)
|
||||
uri = get_uri(decoded, token)
|
||||
summary, full_response = get_info(uri["target"], token)
|
||||
print(f"SONG FOUND : {summary["name"]}")
|
||||
if uri:
|
||||
start_song(uri['target'],os.getenv('DEVICE_ID'))
|
||||
return summary
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
||||
|
||||
cap = cv2.VideoCapture('/dev/video0')
|
||||
|
||||
|
||||
token = os.getenv('ACCESS_TOKEN')
|
||||
if not token:
|
||||
token = "BQCph85n2Cr-h8jKWhsdsdhywaX3h_bn4pJ_-jdque2u_gn9bI-OdthIowGSU6r058QozL0eJfzy_ClWezXYKrQO2npuyfWVphxSQrKqhBWkGr5bK0UrIfsKKAdJvoNrXD9Db-ObgP5D3-rMpF0Xq3RXwMpTal9NpzTJcHZs_PBjbNClJVy24Jk5WfGbKZPkMs_Hon5TjABx4QzxzE2vxjd4X4EyPlyPuKiIVp-f7yTSJbbRLqt-_O_VJ9mnQ1RgGK16afY7p3JZH_B6-VSCrFuhK_m9yhSieiWoqEeopFEX47Nc4-tuqe8CXcYGiRLZBIcc4w64ly36ZIftxRN7ehcJb2gcV26ZqMS1lg1Yxp0OD4ShJJsinA69X535_w"
|
||||
print(token)
|
||||
|
||||
|
||||
if not cap.isOpened():
|
||||
print("Error: Could not open camera.")
|
||||
exit(1)
|
||||
else:
|
||||
print("Camera is working and ready to capture frames.")
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
# Crop same as streaming for consistency
|
||||
height, width, _ = frame.shape
|
||||
margin = 100
|
||||
frame = frame[margin:height - margin, margin:width - margin]
|
||||
|
||||
try:
|
||||
|
||||
|
||||
summary = process_frame(frame, token)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("")
|
||||
continue
|
||||
# cv2.imshow("preframe", frame)
|
||||
|
||||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
break
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
@@ -8,24 +8,46 @@ from skimage.color import rgb2gray
|
||||
import matplotlib.patches as patches
|
||||
from sklearn.cluster import KMeans
|
||||
import warnings
|
||||
from skimage.morphology import remove_small_objects
|
||||
from skimage.transform import resize
|
||||
from skimage import exposure
|
||||
from skimage.morphology import erosion, square
|
||||
import cv2
|
||||
from skimage.filters import threshold_local
|
||||
from scipy import ndimage as ndi
|
||||
from skimage.segmentation import watershed
|
||||
from skimage.morphology import opening, rectangle
|
||||
from scipy.ndimage import gaussian_filter
|
||||
|
||||
warnings.filterwarnings("ignore", category=FutureWarning, message=".*square.*deprecated.*")
|
||||
|
||||
|
||||
#### keep this. If does not work, remove black and reaply the logic only to not-black part of the picture
|
||||
def get_heights(image):
|
||||
# image = io.imread(filename)
|
||||
|
||||
|
||||
im = rgb2gray(image)
|
||||
binary_im = im > threshold_otsu(im)
|
||||
smooth_im = gaussian(binary_im.astype(float), sigma=1)
|
||||
morph_im = closing(smooth_im > 0.5, square(3))
|
||||
labeled = label(morph_im)
|
||||
|
||||
# Filtrage gaussien léger pour diminuer bruit
|
||||
im_blur = gaussian_filter(im, sigma=1)
|
||||
|
||||
# Calcul du seuil local avec block_size impair, ajuster offset si besoin
|
||||
th = threshold_local(im_blur, block_size=71, offset=-0.09)
|
||||
|
||||
binary_im = im_blur > th
|
||||
|
||||
separated = binary_im
|
||||
labeled = label(separated)
|
||||
|
||||
bar_dims = [r.bbox for r in regionprops(labeled)]
|
||||
bar_dims.sort(key=lambda x: x[1]) # left to right
|
||||
|
||||
bars = bar_dims[1:] # skip logo
|
||||
bars = bar_dims#[1:] # skip logo
|
||||
bar_heights_raw = []
|
||||
if(len(bars)!=23):
|
||||
return [0]
|
||||
print(len(bars))
|
||||
return [0], (separated.astype("uint8") * 255) # convert to 0/255 uint8
|
||||
|
||||
for bar in bars:
|
||||
top, left, bottom, right = bar
|
||||
effective_height = bottom - top # use bounding box height directly
|
||||
@@ -44,5 +66,5 @@ def get_heights(image):
|
||||
closest_cluster = np.argmin(diffs)
|
||||
predicted_levels.append(int(closest_cluster))
|
||||
|
||||
|
||||
return predicted_levels
|
||||
print(predicted_levels)
|
||||
return predicted_levels, (separated.astype("uint8") * 255)
|
||||
@@ -9,7 +9,7 @@ REDIRECT_URI = 'https://vinyly.couraud.xyz'
|
||||
SCOPE = 'user-modify-playback-state user-read-playback-state'
|
||||
|
||||
|
||||
def main():
|
||||
def start_song(URI="spotify:track:0RoA7ObU6phWpqhlC9zH4Z",device_id="a499b8f3684d8098ffb5f9ff11392ebdd61fc6d1"):
|
||||
auth_manager = SpotifyOAuth(client_id=CLIENT_ID,
|
||||
client_secret=CLIENT_SECRET,
|
||||
redirect_uri=REDIRECT_URI,
|
||||
@@ -26,28 +26,18 @@ def main():
|
||||
|
||||
sp = spotipy.Spotify(auth_manager=auth_manager)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print('Usage: python play_spotify.py "song name"')
|
||||
sys.exit(1)
|
||||
|
||||
song_name = ' '.join(sys.argv[1:])
|
||||
|
||||
results = sp.search(q=song_name, type='track', limit=1)
|
||||
if not results['tracks']['items']:
|
||||
print(f'No track found for: {song_name}')
|
||||
sys.exit(1)
|
||||
|
||||
track_uri = results['tracks']['items'][0]['uri']
|
||||
|
||||
devices = sp.devices()
|
||||
if not devices['devices']:
|
||||
print('No active Spotify devices found. Please start playback on a device.')
|
||||
sys.exit(1)
|
||||
|
||||
active_device_id = devices['devices'][0]['id']
|
||||
current = sp.current_user_playing_track()
|
||||
if current and current.get("item"):
|
||||
current_uri = current["item"]["uri"]
|
||||
|
||||
sp.start_playback(device_id=active_device_id, uris=[track_uri])
|
||||
print(f'Playing "{song_name}" on device ID: {active_device_id}')
|
||||
if current_uri != URI:
|
||||
sp.start_playback(device_id=device_id, uris=[URI])
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
start_song(URI,device_id)
|
||||
1
old/.cache
Normal file
@@ -0,0 +1 @@
|
||||
{"access_token": "BQBR3Dnu7QMSOnq8G_8lOiz858WKJmMxiCgskqjp3QDu17EcQWx3Y8ALVQGSXzUcTgkJb-z5oHjJxEQJNfz8tSYREfJGGMKQfoCo0P2tCUbnB0wdGm7iSmWOlYuvrnDTVleJ9nJBGG6vVGc2ZEIl43T54_TmKbkPzampV8FuuGIATUvzbjRO6uEHqzc3ImuBELCZJjj0n8-h4zImXifkJP6pssEY76_GWgKo_HVx", "token_type": "Bearer", "expires_in": 3600, "scope": "user-modify-playback-state user-read-playback-state", "expires_at": 1763737778, "refresh_token": "AQAxD3wm_8aDlK9w2ZsibKsrPY0SxUJb2UaqiT2ygxK-pEq2ojxPzgLa6DNt9EpP3mjFDCLmGO5glj5fSpR3ZnqEa26belG4EQv5FOeee38ix31o2xuxPbDsY44KR5Uj1KE"}
|
||||
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -21,12 +21,12 @@ def get_heights(filename: str):
|
||||
|
||||
bars = bar_dims[1:] # skip logo
|
||||
bar_heights_raw = []
|
||||
|
||||
print(len(bars))
|
||||
for bar in bars:
|
||||
top, left, bottom, right = bar
|
||||
effective_height = bottom - top # use bounding box height directly
|
||||
bar_heights_raw.append(effective_height)
|
||||
print(len(bars))
|
||||
|
||||
# Cluster measured heights to 8 clusters representing discrete bar levels
|
||||
bar_heights_raw_np = np.array(bar_heights_raw).reshape(-1, 1)
|
||||
|
||||
@@ -38,8 +38,8 @@ def get_heights(filename: str):
|
||||
for h in bar_heights_raw:
|
||||
diffs = np.abs(cluster_centers - h)
|
||||
closest_cluster = np.argmin(diffs)
|
||||
predicted_levels.append(closest_cluster)
|
||||
predicted_levels.append(int(closest_cluster))
|
||||
|
||||
|
||||
print(len(predicted_levels))
|
||||
print((predicted_levels))
|
||||
return predicted_levels
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |