[mod] alles mögliche verbessert

This commit is contained in:
roydfalk 2025-05-22 19:56:16 +00:00
parent b571c54195
commit 3497430fa4
13 changed files with 255 additions and 36 deletions

View file

@ -2,9 +2,7 @@
## Zu erledigen ## Zu erledigen
- asynchrones Erstellen der Audio-Dateien (solange nicht fertig, eine Markierung in der Liste anzeigen) - sanitizing überprüfen
- Persistenz mit SQLite - Persistenz mit SQLite
- Datum für Dokumente ergänzen - Datum für Dokumente ergänzen
- eingebetter Media-Spieler?
- für jedes bestehende Dokument soll es Funktionen zum Aufbereiten und Herunderladen geben in den Formaten pdf/odt (pandoc nutzen) und ogg - für jedes bestehende Dokument soll es Funktionen zum Aufbereiten und Herunderladen geben in den Formaten pdf/odt (pandoc nutzen) und ogg
- bei Löschung eines Dokuments auch die zugehörigen Dateien löschen

View file

@ -9,7 +9,7 @@ proof-of-concept für Partei-Arbeits-Dokumenten-Verwaltung, welche hörbare Vers
### Voraussetzungen ### Voraussetzungen
- curl (Debian-Paket-Name: `curl`) - [curl](https://curl.se/docs/manpage.html) (Debian-Paket-Name: `curl`)
### Anweisungen ### Anweisungen
@ -33,9 +33,9 @@ proof-of-concept für Partei-Arbeits-Dokumenten-Verwaltung, welche hörbare Vers
### Voraussetzungen ### Voraussetzungen
- PHP auf Kommandozeile (Debian-Paket-Name: `php-cli`) - [PHP](https://www.php.net/)-Interpreter für Kommandozeile (Debian-Paket-Name: `php-cli`)
- ffmpeg (Debian-Paket-Name: `ffmpeg`) - [FFmpeg](https://ffmpeg.org/) (Debian-Paket-Name: `ffmpeg`)
- beliebigen Browser - Browser
### Anweisungen ### Anweisungen

View file

@ -6,6 +6,14 @@ require_once('string.php');
require_once('cache.php'); require_once('cache.php');
/**
*/
function path_wrapped(string $path) : ?string
{
return (\file_exists($path) ? $path : null);
}
/** /**
*/ */
function render(string $template_name, array $arguments) : string function render(string $template_name, array $arguments) : string
@ -43,24 +51,36 @@ function navigate(string $target) : void
/** /**
*/ */
function generate_audio(string $input, string $output_path) : void function generate_audio(
string $input_value,
string $output_path,
?array $options = null
) : void
{ {
$key = \hash('sha256', $input); $options = \array_merge(
$path_wav = \rosavox\helpers\string_\coin( [
'/tmp/{{key}}.wav', 'blocking' => false,
],
($options ?? [])
);
$key = \hash('sha256', $input_value);
$input_path = \rosavox\helpers\string_\coin(
'/tmp/{{key}}.txt',
[ [
'key' => $key, 'key' => $key,
] ]
); );
$path_ogg = $output_path; \file_put_contents($input_path, $input_value);
$command = \rosavox\helpers\string_\coin( $command = \rosavox\helpers\string_\coin(
'echo "{{input}}" | piper/piper --model piper/voice.onnx --output_file {{path_wav}} && ffmpeg -y -i {{path_wav}} {{path_ogg}} ; rm -f {{path_wav}}', 'scripts/generate-audio {{input_path}} {{output_path}}{{suffix}}',
[ [
'input' => $input, 'input_path' => $input_path,
'path_wav' => $path_wav, 'output_path' => $output_path,
'path_ogg' => $path_ogg, 'suffix' => ($options['blocking'] ? '' : ' > /dev/null 2>&1 &'),
] ]
); );
// \unlink($input_path);
// \error_log(\sprintf("-- %s\n", $command));
\exec($command); \exec($command);
} }

View file

@ -15,4 +15,19 @@ function coin(string $template, array $arguments) : string
return $result; return $result;
} }
/**
*/
function replace_all(
string $string,
array $replacements
) : string
{
$result = $string;
foreach ($replacement as $replacement_key => $replacement_value) {
$result = \str_replace($replacement_key, $replacement_value, $result);
}
return $result;
}
?> ?>

View file

@ -61,17 +61,105 @@ function render_list() : string
'docs-list-entry', 'docs-list-entry',
[ [
'label_read' => \rosavox\helpers\misc\translate('action.read'), 'label_read' => \rosavox\helpers\misc\translate('action.read'),
'label_hear' => \rosavox\helpers\misc\translate('action.hear'), 'value_text' => $entry['value']['title'],
'value_link_open' => make_link('edit', ['id' => \sprintf('%u', $entry['id'])]), 'value_link_open' => make_link('edit', ['id' => \sprintf('%u', $entry['id'])]),
'value_link_read' => \rosavox\services\doc\readable_path($entry['id']), 'value_link_read' => \rosavox\services\doc\readable_path($entry['id']),
'value_link_hear' => \rosavox\services\doc\audio_path($entry['id']), 'area_download_readable' => (
'value_name' => \rosavox\helpers\string_\coin( (fn ($name, $path) => (
'{{name}}.oga', (! \file_exists($path))
?
\rosavox\helpers\misc\render(
'state-waiting',
[ [
'name' => \rosavox\services\doc\name($entry['id']), 'info' => \rosavox\helpers\misc\translate('state.generating'),
]
)
:
\rosavox\helpers\misc\render(
'download',
[
'text' => \rosavox\helpers\misc\translate('item.readable.short'),
'tooltip' => \rosavox\helpers\string_\coin(
'{{item}} {{action}}',
[
'action' => \rosavox\helpers\misc\translate('action.download'),
'item' => \rosavox\helpers\misc\translate('item.readable.long'),
] ]
), ),
'value_text' => $entry['value']['title'], 'link' => $path,
'name' => \rosavox\helpers\string_\coin(
'{{name}}.md',
[
'name' => $name,
]
),
'type' => 'text/markdown',
]
)
)) (
\rosavox\services\doc\name($entry['id']),
\rosavox\services\doc\readable_path($entry['id'])
)
),
'area_download_audible' => (
(fn ($name, $path) => (
(! \file_exists($path))
?
\rosavox\helpers\misc\render(
'state-waiting',
[
'info' => \rosavox\helpers\misc\translate('state.generating'),
]
)
:
\rosavox\helpers\misc\render(
'download',
[
'text' => \rosavox\helpers\misc\translate('item.audible.short'),
'tooltip' => \rosavox\helpers\string_\coin(
'{{item}} {{action}}',
[
'action' => \rosavox\helpers\misc\translate('action.download'),
'item' => \rosavox\helpers\misc\translate('item.audible.long'),
]
),
'link' => $path,
'name' => \rosavox\helpers\string_\coin(
'{{name}}.oga',
[
'name' => $name,
]
),
'type' => 'audio/ogg',
]
)
)) (
\rosavox\services\doc\name($entry['id']),
\rosavox\services\doc\audible_path($entry['id'])
)
),
'area_hear' => (
(fn ($name, $path) => (
(! \file_exists($path))
?
\rosavox\helpers\misc\render(
'state-waiting',
[
'info' => \rosavox\helpers\misc\translate('state.generating'),
]
)
:
\rosavox\helpers\misc\render(
'player',
[
'source_path' => $path,
]
)
)) (
\rosavox\services\doc\name($entry['id']),
\rosavox\services\doc\audible_path($entry['id'])
)
),
] ]
), ),
\rosavox\services\doc\list_() \rosavox\services\doc\list_()

28
source/scripts/generate-audio Executable file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
## functions
function syntaxerror
{
echo "SYNTAX: generate-audio <input-path> <output-path>"
}
## args
if [ $# -ge 1 ] ; then input_path=$1 && shift ; else syntaxerror ; fi
if [ $# -ge 1 ] ; then output_path=$1 && shift ; else syntaxerror ; fi
## vars
key=$(cat "${input_path}" | sha256sum | cut -d ' ' -f 1)
wav_path=/tmp/${key}.wav
## exec
cat ${input_path} | piper/piper --model piper/voice.onnx --output_file ${wav_path} > /dev/null
ffmpeg -loglevel error -y -i ${wav_path} -vn ${output_path}
rm -f ${wav_path}
echo ${output_path}

View file

@ -25,17 +25,17 @@ function name(int $id) : string
/** /**
*/ */
function audio_path(int $id) : string function readable_path(int $id) : string
{ {
return \sprintf('docs/%s.oga', name($id)); return \sprintf('docs/%s.md', name($id));
} }
/** /**
*/ */
function readable_path(int $id) : string function audible_path(int $id) : string
{ {
return \sprintf('docs/%s.md', name($id)); return \sprintf('docs/%s.oga', name($id));
} }
@ -101,7 +101,28 @@ function generate_readable(int $id, $doc) : void
/** /**
*/ */
function generate_audio(int $id, $doc) : void function remove_readable(
int $id
) : void
{
$path = readable_path($id);
if (! \file_exists($path))
{
// do nothing
}
else
{
\unlink($path);
}
}
/**
*/
function generate_audible(
int $id,
$doc
) : void
{ {
$pause = " .\n"; $pause = " .\n";
$text = \rosavox\helpers\string_\coin( $text = \rosavox\helpers\string_\coin(
@ -132,11 +153,32 @@ function generate_audio(int $id, $doc) : void
); );
\rosavox\helpers\misc\generate_audio( \rosavox\helpers\misc\generate_audio(
$text, $text,
audio_path($id) audible_path($id),
[
'blocking' => false,
]
); );
} }
/**
*/
function remove_audible(
int $id
) : void
{
$path = audible_path($id);
if (! \file_exists($path))
{
// do nothing
}
else
{
\unlink($path);
}
}
/** /**
*/ */
function empty_() : array function empty_() : array
@ -173,7 +215,7 @@ function create(array $doc) : int
{ {
$id = state::$storage->create($doc); $id = state::$storage->create($doc);
generate_readable($id, $doc); generate_readable($id, $doc);
generate_audio($id, $doc); generate_audible($id, $doc);
return $id; return $id;
} }
@ -183,8 +225,10 @@ function create(array $doc) : int
function update(int $id, array $doc) : void function update(int $id, array $doc) : void
{ {
state::$storage->update($id, $doc); state::$storage->update($id, $doc);
remove_readable($id);
remove_audible($id);
generate_readable($id, $doc); generate_readable($id, $doc);
generate_audio($id, $doc); generate_audible($id, $doc);
} }
@ -193,6 +237,8 @@ function update(int $id, array $doc) : void
function delete(int $id) : void function delete(int $id) : void
{ {
state::$storage->delete($id); state::$storage->delete($id);
remove_readable($id);
remove_audible($id);
} }
@ -217,7 +263,7 @@ function add_examples() : void
'authors' => [ 'authors' => [
'Fenriswolf', 'Fenriswolf',
], ],
'content' => 'Der Allvater hat mich betrogen. Ich werde ihn und seine elende Asenbrut verschlingen.', 'content' => 'Der Allvater hat mich betrogen. Ich werde ihn und seine elende Asenbrut verschlingen. Diese Welt wird enden.',
'reasoning' => null, 'reasoning' => null,
] ]
); );

View file

@ -6,6 +6,12 @@
"action.delete": "löschen", "action.delete": "löschen",
"action.hear": "hören", "action.hear": "hören",
"action.read": "lesen", "action.read": "lesen",
"action.download": "herunterladen",
"item.audible.short": "A",
"item.audible.long": "Audio",
"item.readable.short": "T",
"item.readable.long": "Text",
"state.generating": "wird erstellt",
"domain.doc.title": "Titel", "domain.doc.title": "Titel",
"domain.doc.authors": "Autoren", "domain.doc.authors": "Autoren",
"domain.doc.content": "Formulierung", "domain.doc.content": "Formulierung",

View file

@ -82,7 +82,22 @@ button
font-size: 1.0em; font-size: 1.0em;
} }
.docs-list-entry
{
margin-bottom: 16px;
}
.docs-list-entry > * .docs-list-entry > *
{ {
vertical-align: middle; vertical-align: middle;
} }
.state-waiting
{
cursor: progress;
}
.download::before
{
content: "\21A7 ";
}

View file

@ -1,9 +1,9 @@
<li class="docs-list-entry"> <li class="docs-list-entry">
<a href="{{value_link_open}}">{{value_text}}</a> <a href="{{value_link_open}}">{{value_text}}</a>
| |
<a href="{{value_link_read}}">[{{label_read}}]</a> {{area_download_readable}}
| |
<a href="{{value_link_hear}}" download="{{value_name}}" type="audio/ogg">[{{label_hear}}]</a> {{area_download_audible}}
| |
<audio controls="controls" src="{{value_link_hear}}"></audio> {{area_hear}}
</li> </li>

View file

@ -0,0 +1 @@
<a class="download" href="{{link}}" download="{{name}}" type="{{type}}" title="{{tooltip}}">{{text}}</a>

View file

@ -0,0 +1 @@
<audio controls="controls" src="{{source_path}}"></audio>

View file

@ -0,0 +1 @@
<span class="state-waiting" title="{{info}}">[~]</span>