synops-common: delt lib for alle CLI-verktøy (oppgave 21.16)

Ny crate `tools/synops-common` samler duplisert kode som var
spredt over 13 CLI-verktøy:

- db::connect() — PG-pool fra DATABASE_URL (erstatter 10+ identiske blokker)
- cas::path() — CAS-stioppslag med to-nivå hash-katalog
- cas::root() — CAS_ROOT env med default
- cas::hash_bytes() / hash_file() / store() — SHA-256 hashing og lagring
- cas::mime_to_extension() — MIME → filendelse
- logging::init() — tracing til stderr med env-filter
- types::{NodeRow, EdgeRow, NodeSummary} — delte FromRow-structs

Alle verktøy (unntatt synops-tasks som ikke bruker DB) er refaktorert
til å bruke synops-common. Alle kompilerer og tester passerer.
This commit is contained in:
vegard 2026-03-18 10:51:40 +00:00
parent d532e048b1
commit 6496434bd3
49 changed files with 17684 additions and 352 deletions

View file

@ -262,8 +262,7 @@ kaller dem direkte. Samme verktøy, to brukere.
### Infrastruktur
- [x] 21.15 Jobbkø-dispatcher: endre maskinrommets jobbkø-handlere til å spawne CLI-verktøy (`Command::new("synops-X")`) i stedet for inline-kode. Stdout → jobbresultat, stderr → feillogg, exitkode → status.
- [~] 21.16 Felles lib: `synops-common` crate med PG-tilkobling, CAS-helpers, og node/edge-typer. Deles mellom alle CLI-verktøy for å unngå duplisering.
> Påbegynt: 2026-03-18T10:40
- [x] 21.16 Felles lib: `synops-common` crate med PG-tilkobling, CAS-helpers, og node/edge-typer. Deles mellom alle CLI-verktøy for å unngå duplisering.
## Fase 12: Herding

View file

@ -22,6 +22,15 @@ eller maskinrommet-API. Ligger i PATH via symlink eller direkte kall.
| `synops-feature-status` | Sjekk feature-status: spec, oppgaver, commits, feedback | Ferdig |
| `synops-node` | Hent/vis en node med edges (UUID, --depth, --format json/md) | Ferdig |
## Delt bibliotek
| Crate | Beskrivelse |
|-------|-------------|
| `synops-common` | Delt lib: PG-tilkobling (`db`), CAS-helpers (`cas`), logging (`logging`), node/edge-typer (`types`) |
Alle CLI-verktøy (unntatt `synops-tasks`) bruker `synops-common` som dependency.
Se `synops-common/src/lib.rs` for API-oversikt.
## Konvensjoner
- Navnekonvensjon: `synops-<verb>` (f.eks. `synops-context`)
- Shell-scripts eller Rust binaries

2448
tools/synops-audio/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,5 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres",
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v7", "serde"] }
sha2 = "0.10"
hex = "0.4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -12,7 +12,6 @@
use clap::Parser;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use std::process;
use uuid::Uuid;
@ -124,14 +123,7 @@ struct AudioProcessResult {
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "synops_audio=info".parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_audio");
let cli = Cli::parse();
@ -147,7 +139,7 @@ async fn main() {
}
async fn run(cli: Cli) -> Result<(), String> {
let cas_root = std::env::var("CAS_ROOT").unwrap_or_else(|_| "/srv/synops/media/cas".into());
let cas_root = synops_common::cas::root();
// 1. Parse EDL
let edl: EdlDocument = serde_json::from_str(&cli.edl)
@ -171,7 +163,7 @@ async fn run(cli: Cli) -> Result<(), String> {
}
// 3. Sjekk at kildefilen finnes i CAS
let source_path = cas_path(&cas_root, &cli.cas_hash);
let source_path = synops_common::cas::path(&cas_root, &cli.cas_hash);
if !source_path.exists() {
return Err(format!("Kildefil finnes ikke i CAS: {}", cli.cas_hash));
}
@ -292,14 +284,7 @@ async fn run(cli: Cli) -> Result<(), String> {
let requested_by = cli.requested_by
.ok_or("--requested-by er påkrevd sammen med --write")?;
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL må settes med --write".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
let pnode_id = write_to_db(
&db,
@ -610,7 +595,7 @@ async fn resolve_silence_cuts(
min_duration_ms,
} = op
{
let source_path = cas_path(cas_root, &edl.source_hash);
let source_path = synops_common::cas::path(cas_root, &edl.source_hash);
let regions = detect_silence(&source_path, *threshold_db, *min_duration_ms).await?;
for region in regions {
// Behold 200ms stillhet på hver side for naturlig lyd,
@ -753,21 +738,11 @@ fn build_filter_chain(
// ─── CAS-operasjoner ─────────────────────────────────────────────
/// CAS-filsti: {root}/{hash[0..2]}/{hash[2..4]}/{hash}
fn cas_path(root: &str, hash: &str) -> PathBuf {
PathBuf::from(root)
.join(&hash[..2])
.join(&hash[2..4])
.join(hash)
}
/// Beregn SHA-256, lagre i CAS med atomisk rename.
async fn store_in_cas(cas_root: &str, data: &[u8]) -> Result<(String, u64), String> {
let mut hasher = Sha256::new();
hasher.update(data);
let hash = hex::encode(hasher.finalize());
let hash = synops_common::cas::hash_bytes(data);
let dest = cas_path(cas_root, &hash);
let dest = synops_common::cas::path(cas_root, &hash);
// Allerede lagret?
if dest.exists() {
@ -1077,7 +1052,7 @@ mod tests {
#[test]
fn cas_path_format() {
let p = cas_path("/srv/synops/media/cas", "b94d27b9934d3e08");
let p = synops_common::cas::path("/srv/synops/media/cas", "b94d27b9934d3e08");
assert_eq!(
p,
PathBuf::from("/srv/synops/media/cas/b9/4d/b94d27b9934d3e08")

2280
tools/synops-common/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
[package]
name = "synops-common"
version = "0.1.0"
edition = "2024"
description = "Delt bibliotek for Synops CLI-verktøy: PG-tilkobling, CAS-helpers, node/edge-typer"
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json"] }
uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sha2 = "0.10"
hex = "0.4"
tokio = { version = "1", features = ["fs"] }

View file

@ -0,0 +1,127 @@
//! Content-Addressable Store (CAS) utilities.
//!
//! CAS lagrer filer med SHA-256-hash som nøkkel, organisert i
//! en to-nivå katalogstruktur: `{root}/{hash[0..2]}/{hash[2..4]}/{hash}`.
//!
//! Miljøvariabel: CAS_ROOT (default: /srv/synops/media/cas)
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
/// Standard CAS-rotkatalog.
pub const DEFAULT_CAS_ROOT: &str = "/srv/synops/media/cas";
/// Hent CAS-rotkatalog fra miljøvariabel eller bruk default.
pub fn root() -> String {
std::env::var("CAS_ROOT").unwrap_or_else(|_| DEFAULT_CAS_ROOT.into())
}
/// Beregn filsti i CAS fra rot og hash.
///
/// Struktur: `{root}/{hash[0..2]}/{hash[2..4]}/{hash}`
///
/// # Eksempel
/// ```
/// use synops_common::cas;
/// let p = cas::path("/srv/synops/media/cas", "b94d27b9934d3e08");
/// assert_eq!(p.to_str().unwrap(), "/srv/synops/media/cas/b9/4d/b94d27b9934d3e08");
/// ```
pub fn path(root: &str, hash: &str) -> PathBuf {
let (p1, rest) = hash.split_at(2.min(hash.len()));
let (p2, _) = rest.split_at(2.min(rest.len()));
PathBuf::from(root).join(p1).join(p2).join(hash)
}
/// Beregn SHA-256 hash av bytes. Returnerer hex-streng.
pub fn hash_bytes(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize())
}
/// Beregn SHA-256 hash av en fil. Returnerer hex-streng.
pub async fn hash_file(file_path: &Path) -> Result<String, String> {
let data = tokio::fs::read(file_path)
.await
.map_err(|e| format!("Kunne ikke lese fil {}: {e}", file_path.display()))?;
Ok(hash_bytes(&data))
}
/// Lagre bytes i CAS. Returnerer hash. Oppretter kataloger ved behov.
pub async fn store(cas_root: &str, data: &[u8]) -> Result<String, String> {
let hash = hash_bytes(data);
let target = path(cas_root, &hash);
if target.exists() {
return Ok(hash);
}
if let Some(parent) = target.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("Kunne ikke opprette CAS-katalog: {e}"))?;
}
tokio::fs::write(&target, data)
.await
.map_err(|e| format!("Kunne ikke skrive CAS-fil: {e}"))?;
Ok(hash)
}
/// Konverter MIME-type til filendelse.
pub fn mime_to_extension(mime: &str) -> &str {
match mime {
"audio/mpeg" | "audio/mp3" => "mp3",
"audio/wav" | "audio/x-wav" => "wav",
"audio/ogg" => "ogg",
"audio/flac" | "audio/x-flac" => "flac",
"audio/mp4" | "audio/m4a" | "audio/x-m4a" => "m4a",
"audio/webm" => "webm",
"video/mp4" => "mp4",
"video/webm" => "webm",
"image/jpeg" => "jpg",
"image/png" => "png",
"image/webp" => "webp",
"image/gif" => "gif",
"text/html" => "html",
"application/pdf" => "pdf",
_ => "bin",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cas_path_format() {
let p = path("/srv/synops/media/cas", "b94d27b9934d3e08");
assert_eq!(
p,
PathBuf::from("/srv/synops/media/cas/b9/4d/b94d27b9934d3e08")
);
}
#[test]
fn cas_path_short_hash() {
let p = path("/cas", "ab");
assert_eq!(p, PathBuf::from("/cas/ab//ab"));
}
#[test]
fn hash_known_value() {
let hash = hash_bytes(b"hello world");
assert_eq!(
hash,
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
);
}
#[test]
fn mime_mapping() {
assert_eq!(mime_to_extension("audio/mpeg"), "mp3");
assert_eq!(mime_to_extension("image/png"), "png");
assert_eq!(mime_to_extension("unknown/type"), "bin");
}
}

View file

@ -0,0 +1,33 @@
//! PostgreSQL-tilkobling via DATABASE_URL.
//!
//! Standardisert oppkobling brukt av alle CLI-verktøy.
//! Leser DATABASE_URL fra miljøvariabel, oppretter en pool med
//! et lite antall connections (CLI-verktøy trenger sjelden mer enn 2).
use sqlx::postgres::{PgPool, PgPoolOptions};
/// Opprett en PostgreSQL-pool fra DATABASE_URL miljøvariabel.
///
/// Returnerer feil hvis DATABASE_URL ikke er satt eller tilkobling feiler.
pub async fn connect() -> Result<PgPool, String> {
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL er ikke satt".to_string())?;
PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))
}
/// Opprett en PostgreSQL-pool med egendefinert maks connections.
pub async fn connect_with(max_connections: u32) -> Result<PgPool, String> {
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL er ikke satt".to_string())?;
PgPoolOptions::new()
.max_connections(max_connections)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))
}

View file

@ -0,0 +1,14 @@
//! synops-common — Delt bibliotek for Synops CLI-verktøy.
//!
//! Samler funksjonalitet som deles mellom alle CLI-verktøy:
//! - `db` — PostgreSQL-tilkobling via DATABASE_URL
//! - `cas` — Content-addressable store (sti-oppslag, hashing, MIME)
//! - `logging` — Standardisert tracing-oppsett (stderr, env-filter)
//! - `types` — Node- og edge-typer fra databaseskjemaet
//!
//! Ref: docs/retninger/unix_filosofi.md, tools/README.md
pub mod cas;
pub mod db;
pub mod logging;
pub mod types;

View file

@ -0,0 +1,23 @@
//! Standardisert tracing-oppsett for CLI-verktøy.
//!
//! Alle CLI-verktøy logger til stderr med env-filter.
//! RUST_LOG styrer nivå; uten den brukes `{crate_name}=info`.
/// Initialiser tracing med standard oppsett for et CLI-verktøy.
///
/// - Output til stderr (stdout er reservert for verktøyets resultat)
/// - Env-filter fra RUST_LOG, fallback til `{crate_filter}=info`
/// - Uten target-prefix (kortere linjer)
///
/// `crate_filter` bør være crate-navnet med bindestreker erstattet
/// med understreker, f.eks. `"synops_audio"`.
pub fn init(crate_filter: &str) {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("{crate_filter}=info").parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
}

View file

@ -0,0 +1,51 @@
//! Node- og edge-typer fra databaseskjemaet.
//!
//! Disse typene speiler SQL-skjemaet i `docs/primitiver/nodes.md` og
//! `docs/primitiver/edges.md`. De er ment som delte FromRow-structs
//! for de vanligste spørringene.
//!
//! node_kind og edge_type er freeform-strenger — ikke enums — fordi
//! listen vokser organisk. Se docs for kjente verdier.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// En rad fra `nodes`-tabellen.
///
/// Bruk med `sqlx::query_as::<_, NodeRow>(...)`. Husk å caste
/// enums i SQL: `node_kind::text`, `visibility::text`.
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct NodeRow {
pub id: Uuid,
pub node_kind: String,
pub title: Option<String>,
pub content: Option<String>,
pub visibility: String,
pub metadata: Option<serde_json::Value>,
pub created_at: DateTime<Utc>,
pub created_by: Option<Uuid>,
}
/// En rad fra `edges`-tabellen.
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct EdgeRow {
pub id: Uuid,
pub source_id: Uuid,
pub target_id: Uuid,
pub edge_type: String,
pub metadata: Option<serde_json::Value>,
pub system: bool,
pub created_at: DateTime<Utc>,
pub created_by: Option<Uuid>,
}
/// Forenklet node for listevisning og søkeresultater.
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct NodeSummary {
pub id: Uuid,
pub node_kind: String,
pub title: Option<String>,
pub visibility: String,
pub created_at: DateTime<Utc>,
}

2449
tools/synops-context/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@ name = "synops-context"
path = "src/main.rs"
[dependencies]
synops-common = { path = "../synops-common" }
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json"] }
@ -16,4 +17,3 @@ serde_json = "1"
uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View file

@ -76,14 +76,7 @@ struct RelatedNodeRow {
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "synops_context=info".parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_context");
let cli = Cli::parse();
@ -94,14 +87,7 @@ async fn main() {
}
async fn run(cli: Cli) -> Result<(), String> {
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL er ikke satt".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
let comm_id = cli.communication_id;

2461
tools/synops-feature-status/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@ name = "synops-feature-status"
path = "src/main.rs"
[dependencies]
synops-common = { path = "../synops-common" }
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json"] }

View file

@ -35,6 +35,7 @@ struct Cli {
}
fn main() {
synops_common::logging::init("synops_feature_status");
let cli = Cli::parse();
let repo = PathBuf::from(&cli.repo);
@ -430,48 +431,31 @@ fn parse_commit_lines(
// --- Feedback fra database ---
fn run_feedback(key: &str) {
let db_url = match resolve_database_url() {
Some(url) => url,
None => {
println!("## Feedback\n");
println!("_(DATABASE_URL ikke satt — feedback hoppes over)_\n");
return;
}
};
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(print_feedback(&db_url, key));
}
fn resolve_database_url() -> Option<String> {
if let Ok(url) = std::env::var("DATABASE_URL") {
return Some(url);
}
// Prøv maskinrommet.env
// Sikre at DATABASE_URL er satt — prøv maskinrommet.env som fallback
if std::env::var("DATABASE_URL").is_err() {
if let Ok(content) = std::fs::read_to_string("/tmp/maskinrommet.env") {
for line in content.lines() {
if let Some(val) = line.strip_prefix("DATABASE_URL=") {
return Some(val.to_string());
// SAFETY: kalles tidlig i main, før noen tråder er startet.
unsafe { std::env::set_var("DATABASE_URL", val); }
break;
}
}
}
}
None
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(print_feedback(key));
}
async fn print_feedback(db_url: &str, key: &str) {
async fn print_feedback(key: &str) {
println!("## Feedback\n");
let db = match sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(db_url)
.await
{
let db = match synops_common::db::connect().await {
Ok(pool) => pool,
Err(e) => {
eprintln!("Kunne ikke koble til database: {e}");
println!("_(Databasefeil — feedback hoppes over)_\n");
eprintln!("{e}");
println!("_(DATABASE_URL ikke satt eller databasefeil — feedback hoppes over)_\n");
return;
}
};

2450
tools/synops-node/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -17,3 +17,4 @@ uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -117,14 +117,7 @@ struct FullOutput {
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "synops_node=info".parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_node");
let cli = Cli::parse();
@ -135,14 +128,7 @@ async fn main() {
}
async fn run(cli: Cli) -> Result<(), String> {
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL er ikke satt".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
// Hent hovednoden
let root_node = fetch_node(&db, cli.node_id).await?

View file

@ -1605,6 +1605,22 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synops-common"
version = "0.1.0"
dependencies = [
"chrono",
"hex",
"serde",
"serde_json",
"sha2",
"sqlx",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "synops-prune"
version = "0.1.0"
@ -1614,6 +1630,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"synops-common",
"tokio",
"tracing",
"tracing-subscriber",

View file

@ -17,3 +17,4 @@ uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -457,14 +457,7 @@ async fn phase_critical(
async fn main() {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_prune");
let dry_run = cli.is_dry_run();
@ -476,14 +469,7 @@ async fn main() {
}
async fn run(cli: Cli, dry_run: bool) -> Result<(), String> {
let database_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL er ikke satt".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&database_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
// Sjekk at CAS-katalogen eksisterer
if !cli.cas_root.exists() {

View file

@ -1839,16 +1839,31 @@ dependencies = [
]
[[package]]
name = "synops-render"
name = "synops-common"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"hex",
"serde",
"serde_json",
"sha2",
"sqlx",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "synops-render"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"serde",
"serde_json",
"sqlx",
"synops-common",
"tera",
"tokio",
"tracing",

View file

@ -16,7 +16,6 @@ serde_json = "1"
uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
tera = "1"
sha2 = "0.10"
hex = "0.4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -13,7 +13,6 @@
use chrono::{DateTime, Utc};
use clap::Parser;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use std::process;
use tera::{Context, Tera};
@ -375,18 +374,10 @@ mod tiptap;
// CAS-lagring
// =============================================================================
fn cas_path(root: &str, hash: &str) -> PathBuf {
let (p1, rest) = hash.split_at(2.min(hash.len()));
let (p2, _) = rest.split_at(2.min(rest.len()));
PathBuf::from(root).join(p1).join(p2).join(hash)
}
async fn store_in_cas(root: &str, data: &[u8]) -> Result<(String, u64, bool), String> {
let mut hasher = Sha256::new();
hasher.update(data);
let hash = hex::encode(hasher.finalize());
let hash = synops_common::cas::hash_bytes(data);
let size = data.len() as u64;
let path = cas_path(root, &hash);
let path = synops_common::cas::path(root, &hash);
if path.exists() {
return Ok((hash, size, true));
@ -971,14 +962,7 @@ async fn log_resource_usage(db: &sqlx::PgPool, node_id: Uuid, bytes: u64, operat
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_render");
let cli = Cli::parse();
@ -989,26 +973,12 @@ async fn main() {
}
// Koble til database
let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
if cli.write {
eprintln!("DATABASE_URL er påkrevd med --write");
process::exit(1);
}
// Uten --write trenger vi fortsatt DB for oppslag
eprintln!("DATABASE_URL mangler — påkrevd for databaseoppslag");
let db = synops_common::db::connect().await.unwrap_or_else(|e| {
eprintln!("{e}");
process::exit(1);
});
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.unwrap_or_else(|e| {
eprintln!("Kunne ikke koble til database: {e}");
process::exit(1);
});
let cas_root = std::env::var("CAS_ROOT").unwrap_or_else(|_| "/srv/synops/media/cas".into());
let cas_root = synops_common::cas::root();
let result = match cli.render_type.as_str() {
"article" => {
@ -1112,7 +1082,7 @@ mod tests {
#[test]
fn test_cas_path() {
let path = cas_path("/srv/synops/media/cas", "b94d27b9934d3e08");
let path = synops_common::cas::path("/srv/synops/media/cas", "b94d27b9934d3e08");
assert!(path.to_string_lossy().contains("/b9/4d/"));
}

2450
tools/synops-respond/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -17,3 +17,4 @@ uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -71,14 +71,7 @@ struct ParticipantRow {
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "synops_respond=info".parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_respond");
let cli = Cli::parse();
@ -89,14 +82,7 @@ async fn main() {
}
async fn run(cli: Cli) -> Result<(), String> {
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL er ikke satt".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
let communication_id = cli.communication_id;
let agent_node_id = cli.agent_node_id;

View file

@ -1605,6 +1605,22 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synops-common"
version = "0.1.0"
dependencies = [
"chrono",
"hex",
"serde",
"serde_json",
"sha2",
"sqlx",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "synops-rss"
version = "0.1.0"
@ -1614,6 +1630,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"synops-common",
"tokio",
"tracing",
"tracing-subscriber",

View file

@ -17,3 +17,4 @@ uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -89,14 +89,7 @@ struct FeedItem {
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_rss");
let cli = Cli::parse();
@ -105,19 +98,7 @@ async fn main() {
process::exit(1);
}
let db_url = match std::env::var("DATABASE_URL") {
Ok(url) => url,
Err(_) => {
eprintln!("Feil: DATABASE_URL er ikke satt");
process::exit(1);
}
};
let db = match sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
{
let db = match synops_common::db::connect().await {
Ok(pool) => pool,
Err(e) => {
eprintln!("Feil: Kunne ikke koble til database: {e}");

2450
tools/synops-search/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -17,3 +17,4 @@ uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -45,14 +45,7 @@ struct SearchResult {
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "synops_search=info".parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_search");
let cli = Cli::parse();
@ -63,14 +56,7 @@ async fn main() {
}
async fn run(cli: Cli) -> Result<(), String> {
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL er ikke satt".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
let query = &cli.query;

View file

@ -1905,6 +1905,22 @@ dependencies = [
"futures-core",
]
[[package]]
name = "synops-common"
version = "0.1.0"
dependencies = [
"chrono",
"hex",
"serde",
"serde_json",
"sha2",
"sqlx",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "synops-suggest-edges"
version = "0.1.0"
@ -1915,6 +1931,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"synops-common",
"tokio",
"tracing",
"tracing-subscriber",

View file

@ -18,3 +18,4 @@ chrono = { version = "0.4", features = ["serde"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -140,14 +140,7 @@ Regler:
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "synops_suggest_edges=info".parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_suggest_edges");
let cli = Cli::parse();
@ -163,14 +156,7 @@ async fn main() {
}
async fn run(cli: Cli) -> Result<(), String> {
let db_url =
std::env::var("DATABASE_URL").map_err(|_| "DATABASE_URL er ikke satt".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
let node_id = cli.node_id;

View file

@ -1905,6 +1905,22 @@ dependencies = [
"futures-core",
]
[[package]]
name = "synops-common"
version = "0.1.0"
dependencies = [
"chrono",
"hex",
"serde",
"serde_json",
"sha2",
"sqlx",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "synops-summarize"
version = "0.1.0"
@ -1915,6 +1931,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"synops-common",
"tokio",
"tracing",
"tracing-subscriber",

View file

@ -18,3 +18,4 @@ chrono = { version = "0.4", features = ["serde"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -110,14 +110,7 @@ Regler:
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "synops_summarize=info".parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_summarize");
let cli = Cli::parse();
@ -133,14 +126,7 @@ async fn main() {
}
async fn run(cli: Cli) -> Result<(), String> {
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL er ikke satt".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
let communication_id = cli.communication_id;

231
tools/synops-tasks/Cargo.lock generated Normal file
View file

@ -0,0 +1,231 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "clap"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synops-tasks"
version = "0.1.0"
dependencies = [
"clap",
"regex",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]

View file

@ -1923,6 +1923,22 @@ dependencies = [
"futures-core",
]
[[package]]
name = "synops-common"
version = "0.1.0"
dependencies = [
"chrono",
"hex",
"serde",
"serde_json",
"sha2",
"sqlx",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "synops-transcribe"
version = "0.1.0"
@ -1933,6 +1949,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"synops-common",
"tokio",
"tracing",
"tracing-subscriber",

View file

@ -18,3 +18,4 @@ chrono = { version = "0.4", features = ["serde"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "multipart"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
synops-common = { path = "../synops-common" }

View file

@ -14,7 +14,6 @@
use chrono::Utc;
use clap::Parser;
use serde::Serialize;
use std::path::PathBuf;
use std::process;
use uuid::Uuid;
@ -76,14 +75,7 @@ struct TranscribeResult {
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "synops_transcribe=info".parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_transcribe");
let cli = Cli::parse();
@ -99,11 +91,11 @@ async fn main() {
}
async fn run(cli: Cli) -> Result<(), String> {
let cas_root = std::env::var("CAS_ROOT").unwrap_or_else(|_| "/srv/synops/media/cas".into());
let cas_root = synops_common::cas::root();
let whisper_url = std::env::var("WHISPER_URL").unwrap_or_else(|_| "http://localhost:8000".into());
// 1. Les lydfil fra CAS
let file_path = cas_path(&cas_root, &cli.cas_hash);
let file_path = synops_common::cas::path(&cas_root, &cli.cas_hash);
let file_data = tokio::fs::read(&file_path)
.await
.map_err(|e| format!("Kunne ikke lese CAS-fil {}: {e}", cli.cas_hash))?;
@ -116,7 +108,7 @@ async fn run(cli: Cli) -> Result<(), String> {
);
// 2. Send til faster-whisper API (SRT-format)
let file_ext = mime_to_extension(&cli.mime);
let file_ext = synops_common::cas::mime_to_extension(&cli.mime);
let file_name = format!("audio.{file_ext}");
let file_part = reqwest::multipart::Part::bytes(file_data)
@ -187,14 +179,7 @@ async fn run(cli: Cli) -> Result<(), String> {
// 6. Skriv til database hvis --write
if cli.write {
let node_id = cli.node_id.unwrap(); // Allerede validert
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL må settes med --write".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
// Slett gamle segmenter for denne noden (idempotent)
sqlx::query("DELETE FROM transcription_segments WHERE node_id = $1")
@ -218,26 +203,6 @@ async fn run(cli: Cli) -> Result<(), String> {
Ok(())
}
/// CAS-filsti: {root}/{hash[0..2]}/{hash[2..4]}/{hash}
fn cas_path(root: &str, hash: &str) -> PathBuf {
PathBuf::from(root)
.join(&hash[..2])
.join(&hash[2..4])
.join(hash)
}
fn mime_to_extension(mime: &str) -> &str {
match mime {
"audio/mpeg" | "audio/mp3" => "mp3",
"audio/wav" | "audio/x-wav" => "wav",
"audio/ogg" => "ogg",
"audio/flac" | "audio/x-flac" => "flac",
"audio/mp4" | "audio/m4a" | "audio/x-m4a" => "m4a",
"audio/webm" => "webm",
_ => "wav",
}
}
// --- SRT-parsing ---
fn parse_srt(srt: &str) -> Result<Vec<Segment>, String> {
@ -467,10 +432,10 @@ I dag snakker vi om fotball.
#[test]
fn test_cas_path() {
let p = cas_path("/srv/synops/media/cas", "b94d27b9934d3e08");
let p = synops_common::cas::path("/srv/synops/media/cas", "b94d27b9934d3e08");
assert_eq!(
p,
PathBuf::from("/srv/synops/media/cas/b9/4d/b94d27b9934d3e08")
std::path::PathBuf::from("/srv/synops/media/cas/b9/4d/b94d27b9934d3e08")
);
}
}

View file

@ -1906,13 +1906,11 @@ dependencies = [
]
[[package]]
name = "synops-tts"
name = "synops-common"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"hex",
"reqwest",
"serde",
"serde_json",
"sha2",
@ -1923,6 +1921,22 @@ dependencies = [
"uuid",
]
[[package]]
name = "synops-tts"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"reqwest",
"serde",
"serde_json",
"sqlx",
"synops-common",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "synstructure"
version = "0.13.2"

View file

@ -8,6 +8,7 @@ name = "synops-tts"
path = "src/main.rs"
[dependencies]
synops-common = { path = "../synops-common" }
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json"] }
@ -16,7 +17,4 @@ serde_json = "1"
uuid = { version = "1", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
sha2 = "0.10"
hex = "0.4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View file

@ -14,8 +14,6 @@
// Ref: docs/retninger/unix_filosofi.md, docs/proposals/ghost_host_tts.md
use clap::Parser;
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use std::process;
use uuid::Uuid;
@ -53,14 +51,7 @@ const MAX_TEXT_LENGTH: usize = 5000;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "synops_tts=info".parse().unwrap()),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
synops_common::logging::init("synops_tts");
let cli = Cli::parse();
@ -107,8 +98,8 @@ async fn run(cli: Cli) -> Result<(), String> {
tracing::info!(audio_size = audio_bytes.len(), "Mottok lyd fra ElevenLabs");
// 2. Lagre i CAS
let cas_root = std::env::var("CAS_ROOT").unwrap_or_else(|_| "/srv/synops/media/cas".into());
let cas_hash = store_in_cas(&cas_root, &audio_bytes).await?;
let cas_root = synops_common::cas::root();
let cas_hash = synops_common::cas::store(&cas_root, &audio_bytes).await?;
tracing::info!(cas_hash = %cas_hash, "Lyd lagret i CAS");
@ -189,45 +180,6 @@ async fn call_elevenlabs(text: &str, voice_id: &str) -> Result<Vec<u8>, String>
.map_err(|e| format!("Kunne ikke lese ElevenLabs-respons: {e}"))
}
/// Lagre bytes i CAS. Returnerer SHA-256 hash.
/// Atomisk skriving via temp-fil + rename.
async fn store_in_cas(cas_root: &str, data: &[u8]) -> Result<String, String> {
let mut hasher = Sha256::new();
hasher.update(data);
let hash = hex::encode(hasher.finalize());
let dir = PathBuf::from(cas_root)
.join(&hash[..2])
.join(&hash[2..4]);
let final_path = dir.join(&hash);
// Allerede lagret? Returner direkte.
if final_path.exists() {
tracing::info!("CAS-deduplisering: filen finnes allerede");
return Ok(hash);
}
tokio::fs::create_dir_all(&dir)
.await
.map_err(|e| format!("Kunne ikke opprette CAS-dir {}: {e}", dir.display()))?;
let tmp_dir = PathBuf::from(cas_root).join("tmp");
tokio::fs::create_dir_all(&tmp_dir)
.await
.map_err(|e| format!("Kunne ikke opprette CAS tmp-dir: {e}"))?;
let tmp_path = tmp_dir.join(format!("{hash}.tmp"));
tokio::fs::write(&tmp_path, data)
.await
.map_err(|e| format!("Kunne ikke skrive CAS temp-fil: {e}"))?;
tokio::fs::rename(&tmp_path, &final_path)
.await
.map_err(|e| format!("Kunne ikke flytte CAS-fil: {e}"))?;
Ok(hash)
}
/// Opprett media-node og has_media-edge i PostgreSQL.
/// Returnerer media_node_id for STDB-synk.
async fn write_to_db(
@ -239,14 +191,7 @@ async fn write_to_db(
source_node_id: Option<Uuid>,
requested_by: Uuid,
) -> Result<Uuid, String> {
let db_url = std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL må settes med --write".to_string())?;
let db = sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.map_err(|e| format!("Kunne ikke koble til database: {e}"))?;
let db = synops_common::db::connect().await?;
let media_node_id = Uuid::now_v7();
let title = format!("TTS: {}", truncate(text, 60));